Dados B3 › Transparency
Transparency
Transparency — how you check the numbers yourself
Data version 2026-09-27-reapresentacao-agreg · page generated 2026-09-27 06:52 UTC. If these do not match /saude, you are reading a cached copy.
What is checked, before every publication — 479 automated tests, collected live
16 are DATA invariants (does the number tie out?) and 120 are product tests — access gate, navigation, language, AI connector. Calling all 479 'invariants' would be inflating it: the split is checkable in the list itself. Not a hand-picked highlight reel: every test below was found by introspecting the actual test files just now, the same way pytest --collect-only would. Grouped by what it protects against, not by which file it lives in.
Level 1 — Integrity: does the data tie out? (16)
TR34— Every public route WITH A PATH PARAMETER opens under any Accept header and is counted by the panel. TR16 audits public HTML pages but drops every parameterised route by construction and only looks at routes declaring an HTML response class, so the company pages, the fund pages and the 500 audit samples were all invisible to it. The samples served raw JSON to */* and were measured by nothing at all: the panel read zero for them since forever, and that zero meant not-measured, not nobody-came. This test also refuses to let the list of parameterised routes go stale, and proves the data-route label is honest rather than a hiding place.TR35— The bulk-export index states how many rows come without a traded code. Its note used to promise that every line carried the ticker and the CVM code, so the datasets would join against your own base — false for one row in six, 11,739 of 71,085 in the indicators set, across 96 companies. It is not a broken join: those are companies registered with the regulator that have no traded stock, and not one of them has a single trading session, so there is no code to carry. The data is right; the sentence promised more. The cost falls on whoever consumes it and is invisible — joining on ticker drops those rows with no error at all, and an outside audit that found the gap concluded it was a failing join, which is itself the symptom of an undeclared limit. Same defect as the price-session wording fixed a day earlier: a claim above what the data delivers, always in our favour, and the same fix — publish the size, computed from the base.TR36— `/empresas/{ticker}` answers instead of returning 404. Walking one level up a URL is the most natural move for anyone exploring an API, and it used to say not-found for a company that exists — an agent concludes coverage ended there. The root is a DATA route (JSON for everyone, any Accept, like the rest of /empresas/), it is OPEN because it carries no figure, and it says three things: who the company is, where its human-readable page lives, and which resources exist, each declaring whether it needs a key.TR37— The /saude alarms on a synthetic database, one at a time: a build started 3 times (the limit) without finishing lights up, 2 does not, a finished one does not; “running” for more than 6 h lights up, “failed” 7 h ago does not say “running”, “running” for 1 h does not; the last session 7 days ago lights the daily alarm, 6 does not; 3 daily-update failures in a row light up, 2 do not; a database with no price gives null coverage instead of breaking the route.TR38— _com_idioma adds the language switch exactly ONCE and is idempotent, pointing to the other language of the same route; public pages of each family carry one switch; the watcher JSON shows at most 500 events when no limit is given.TR39— _idioma: an explicit ?lang= wins in any case; otherwise the language that appears FIRST in Accept-Language; neither pt nor en gives PT. _com_idioma without a breadcrumb puts the switch right after “back”, once; with two breadcrumbs it marks only the first. _noindex_follow touches only the first <head>. _data_por_extenso drops a time stamp and returns None without a date.TR40— The visit middleware counts, on GET only: each company page, its restatement page, the hubs, the comparisons and the FUND page when the answer is HTML; not an arbitrary /acoes sub-path, not POST, not the fund JSON on the same URL. Key usage counts only answers below 400 and never on the pages of _PAGINAS_ANALYTICS.TR41— On the page URL with a key: an unknown fund or ETF is 404; a database of another version answers 503 “building” (page and JSON); an old fund code redirects with 301 to the current one, and a current code equal to the requested one does NOT redirect.TR42— /vigia/eventos: `desde` includes the day itself; `limite` stays between 1 and 5,000; `tipos` carries the name of each type.TR43— A database with no build or daily-update keys: failures and starts are 0 and there is no warning; the disk is in GB with two decimals and the used share with one, of the same data folder; price coverage carries the FIRST and the last session.TR44— Tasting: /empresas/WEGE3 is an open data route; POST and error answers do not count. Daily limit: exactly the limit passes, the next one is 429, and the message names the plan (“free” or “Pro”). A public page that already declares Vary: Accept goes out with Accept, Accept-Language.TR45— The dadosb3 client (cliente/) runs against the app through the TestClient transport, with no network: verdict, watcher (company and funds), REIT restatements, screener with as_of and receipt, and reading the receipt. The recorded answers the package tests use offline have the SAME top-level keys the route answers today, and every key quoted in the package README exists.TR46— /api has one section per public and keyed route (company, indicators, multiples, dividends, screener with as_of and receipt, receipt, verdict, watcher, company and REIT restatements, REIT, ETF). In PT and EN, each section shows the curl, the five-line Python and the answer the route gives NOW, cut by the same cut function; the Python runs on the pip client and returns the same answer. The access written in each section is what the route does in commercial mode (open, key except for the open tickers, key always), and the screener example still comes out without opening the screener to anyone without a key. The key rule, the cache, the connector version and the MCP registry come from the app constants, and the cache header is what the page says. No banned term.TR47— The code only imports PUBLIC FastAPI and Starlette modules (no fastapi.middleware.* or starlette.middleware.* beyond the documented ones, no private segment starting with an underscore); requirements pins both within a range with an upper bound; and building the /api examples (the whole app through the public httpx ASGI transport) records no visit, key use or trial use, and brings the answer of every section.TR48— The /api examples are generated in a thread right after start-up (fired by the lifespan outside the test battery): after it, the first visit to /api generates nothing. With no database, the warm-up does not generate and /api generates on demand. An incomplete example is redone only after the interval; a complete one holds for the whole data version.TR49— /metodologia/vigia and /metodologia/veredito exist in PT and EN, are in the index, the sitemap and llms.txt, and are linked from the three /vigia tabs, the per-ticker watcher page and the verdict block. The cut-offs quoted are the configuration ones, GENERATED when the page is served: changing a cut-off in the file changes the text; no marker comes out raw (not even in the .md); an unknown marker fails loudly. No banned term.
Level 2 — Semantics: does the account mean what the calculation assumes? (0)
Level 3 — Economic: is the result possible in the real world? (0)
Other product tests (34)
Access gate, navigation and the MCP connector — they test whether the product works, not whether a number is right, so they don't fit the 3 levels above by design. Still counted, still listed:
I-TR01— Every test whose ID belongs to the data-invariant family (any "I-"/"H-" that is not navigation) must be classified in `_NIVEL_DO_TESTE`.I-TR02— The total collected by introspection is EXACTLY the number of tests `pytest --collect-only` sees in the same modules — not one more.I-TR03— No test disappears from the collection for lack of a docstring or ID — the explicit requirement is 'never vanish silently'.I-TR04— The featured-case lists may only cite IDs the real suite still has — a dangling case (a test renamed or removed) is dead history that should not keep being told.I-TR05— Every test in the suite has a description in Portuguese AND in English — the double gate exists because requiring only the translation would let an empty description come back, translated as empty.TR05— Every `testes/test_*.py` file is declared on the transparency page — otherwise the page announces a smaller count than the real suite.TR06— No public page announces a company or test count with a number typed into the HTML.TR07— The global metrics are IDENTICAL in Portuguese and English.TR08— A number on an EN page comes out in the reader's format, with a comma.TR09— Two tests may not carry the same identifier.TR10— The 100+ test list is filterable, and the filter never hides anything from someone without JavaScript.TR11— No analytical page takes more than 2s to build.TR12— Sweeps ALL public pages for any number that coincides with a database count without being current.TR13— No page may claim exclusivity beyond the universe /comparativo declares it compared.TR14— A claim of SUPERIORITY, not merely of exclusivity, must say who it was compared against.TR15— The traffic panel has a count that does NOT rise when it is us browsing or when the suite runs.TR16— Every public HTML page is measured — the funnel pages above all.TR17— A paid click is identifiable, and nothing beyond the campaign is stored.TR18— A refusal by plan or by quota is counted, and the analytics database NEVER stores the key in clear text — counting usage per user without holding a credential is what keeps the published 'no cookies, no IP, no third parties' promise true inside the panel too.TR19— Every expected value in the public challenge is what the API serves right now. The challenge exists for a third party to contradict us — it publishes the hard cases with the number we claim and the recipe to recompute it from CVM filings. That only holds while the claimed number is true. A challenge whose expected value has aged does not look bad on the page; it becomes marketing that looks like an audit. So divergence here BLOCKS PUBLICATION rather than rendering nicely.TR20— Each of the challenge verifiers rejects the defect it watches. The challenge is only worth something if the verifiers bite: one that passes the defect is decorative, and decorative is worse than absent because it buys trust while giving nothing. desafio/atacar.py holds the attack table — one real defect per verification type — and this test runs the whole table.TR21— The transparency page stamps the data version and the moment it was generated. Born from an external review: the reviewer read a stale INDEXED copy of the page, carrying counts from weeks earlier, and reported it as an inconsistency against /saude, which was serving the current ones. The pages were right; the copy was old. But he had no way to know: without a stamp, a cached page is indistinguishable from a live one. In a product whose thesis is auditability that is serious — the reader must not have to guess whether what they are reading is current.TR22— The challenge publishes how many TRAP TYPES it covers, not just how many cases. An external reviewer pointed out that 20 cases package fewer than 20 distinct traps — three look-ahead cases in different sectors, two absent-indicator cases for banks, one declared control — and called the '20' marketing. He is right: the repetition is deliberate and explained in each case, but a reader seeing '20 cases' understands 20 traps. A challenge that inflates its own coverage has exactly the defect it exists to hunt.TR23— The same seed returns the same cases, and without a seed nothing is drawn. Three external reviewers raised the same objection: the challenge proves we can defend cases WE chose, and says nothing about the error rate of the base. One refused to use the challenge as an independent sample, calling it circular. The random-audit endpoint takes the choice out of our hands — but only if two properties hold: determinism, so the auditor can publish a result anyone reproduces; and the seed being theirs, because a server that picks the sample could retry until a convenient one appears.TR24— The path form of the random audit returns the same sample as the query-string form. A fourth external reviewer in a row could not test the API live: their browsing tool refuses non-indexed query-string URLs, so the sample was unreachable while the whole site was up. They rigorously reported 0 of 75 cases checked, inconclusive by access limitation, rather than claiming the base passed. When four reviewers trip on the same step, the step is ours. The second door only helps if it is the same door: diverging samples for one seed would be worse than having no second door at all.TR25— Every page that shows a live count stamps the data version and the generation time. The same external reviewer, two rounds running: first they read an old indexed copy of the transparency page and reported it as an inconsistency; then they saw the home page announcing one test count while the indexed Portuguese copy still showed a much older one. They were right about what was missing: to audit anything you must know which state of the system you are auditing. A live number with no generation date ages in silence, and the stale copy becomes indistinguishable from the live page.TR26— An auditor's sample survives an update to the base. Found against our own interest: running the external reviewer's three seeds on the rebuilt base to send them a case-by-case diff, there was no diff at all — none of the 75 cases recurred. The universe had gone from 63,406 rows to 63,375 and the draw was POSITIONAL, so removing 31 rows shifts every other one. That destroyed the only property this endpoint sells: the auditor publishes a result, the base updates, and nobody can re-check what they claimed. Worse, anyone trying would see different cases and conclude the sample had been hand-picked, which is the accusation the endpoint exists to make impossible. Hashing the key instead of the position gives each case a position of its own, so an update moves only the rows it actually touched. The test simulates the update rather than describing it.TR27— The audit sample is a citable artifact, not just a dynamic page. Suggested by the external reviewer and adopted in full: hashing fixed reproducibility, but three years from now the universe will be a different size, and someone who published an audit could not PROVE a given case belonged to that sample. With the sample key and its score the proof is arithmetic and does not depend on us — anyone recomputes the score from the key and the seed. The audit id ties the four parameters that define one run into a short citable label so two different audits cannot be confused. The test also enforces machine-readable quality: prose serves a human, but an API consumer has to decide without interpreting text, so 'this financial year has a scale mismatch, do not use its absolute value' must be a boolean, not a paragraph.TR28— The audit history separates outside auditors from ourselves and keeps the errors on record. The page exists against commercial instinct: the reflex is to publish no errors found and let the findings die in the chat of whoever audited. But trust us, the base is good is worth nothing coming from whoever sells the base, and nobody who hides errors publishes a table of errors. The test closes the two obvious ways this page could turn into marketing: adding internal and external checks into one number, and recording as checked the whole sample when only part of it was actually redone. One reviewer received seventy-five cases and could redo five; writing seventy-five would have been the comfortable lie, and it was the reviewer who refused to tell it.TR29— Wherever an agreement rate is published, the statistical caveat sits in the same block. Raised by the external reviewer, and he was right: this page can produce exactly the impression of certainty it exists to fight. Publishing eighty out of eighty reads as the base has no errors. What it means is only that no divergence was found in those eighty cases, which supports no claim about the population, especially when most of the checks were run by us. The test demands adjacency, not existence: a true caveat alone in a footer is a caveat nobody reads, and serves only to defend us after someone has already misread the number. It is enforced in both languages, since the reader least able to check the rest of the site is the one reading the translation.TR30— The published audit prompt sends the auditor to the primary source, not to us. The outside advice was to find a human third party. Half of that does not hold: an auditor need not be human, it needs to reach the primary source and have no stake in the outcome — the best external finding so far, a utility's EBITDA matching to the cent against the company's own release, came from an AI reading an independent source. The half that does hold is that we cannot sit in the middle: while we write the prompt and hand it over case by case, the framing is ours, and framing is half the result. So the prompt is published, it points at the regulator's archive rather than our endpoint, it tells the auditor to pick a seed nobody suggested, and it forbids turning could not check into passed.TR31— Every seed in the published range is listed, with no gaps, and the hub that lists them is indexable and in the sitemap. A contiguous, complete range is what makes cherry-picking impossible: publishing only 'some example seeds' would hand the choice of sample back to us, which is the very vice this endpoint exists to kill, and a gap in the middle is indistinguishable from carelessness — nobody counts five hundred links by hand. The sitemap half of this test was revised in September 2026: it used to demand all five hundred sample URLs be in the sitemap, which could never work because those pages are noindex; what delivers the original intent is the indexable hub that links them all.TR32— A failing build becomes visible state, not just a log line. Real incident: a change broke one path of the build, the exception was logged, the job retried hourly, the previous database kept serving, and the site looked perfectly normal for sixteen hours. The design protected users, since nothing half-built was ever published, but it protected too well and hid the problem from us as well. The fix is not email: it is publishing the state where someone already looks. The health endpoint is polled by the daily routine and open to anyone, so a silent failure now requires someone to ignore a field that says failed. The test also demands the error itself and a plain-language line explaining what the failure means, because a bare failure count only alarms whoever already knows how to read it.TR33— The comparison page states when it was checked and admits where we lose. It had been wrong in our own favour for three weeks: the table said a competitor had no MCP connector, only a partial methodology, and a quarterly price around forty reais. In fact it now ships a native MCP connector installable in one line, publishes a methodology that cites regulator account codes, and charges essentially our price with a free tier shaped like ours. That page is the SECOND most fetched by AI assistants, which makes it the worst possible place to be wrong in our own favour, and simply re-dating it without re-checking would have preserved the error - the word verified only means something if someone verified. The test attacks the shape a marketing document takes when it pretends to be a comparison: it requires the section listing where each competitor beats us to exist, requires a concrete admission (one of them has price history going back twenty-four years further than ours), and requires the competitor's connector to keep being acknowledged.
Reconstruct it yourself — net margin across 5 companies, 4 sectors
We don't ask you to trust the numbers. Here's the net margin of five companies in four different sectors, each rebuilt straight from its annual report (DFP) at the CVM — net income ÷ revenue, matching the published figure exactly. For ANY indicator, of any company, the whole chain down to the line in CVM's file is at /linhagem:
| Company · sector | Net income (CVM acct) | Revenue (CVM acct) | Margin =÷ |
|---|---|---|---|
| WEGE3 · industrial | R$ 6.78 bn DRE:3.11 | R$ 40.80 bn DRE:3.01 | 16.6% |
| VALE3 · mining | R$ 11.81 bn DRE:3.11 | R$ 213.59 bn DRE:3.01 | 5.5% |
| PETR4 · oil & gas | R$ 110.61 bn DRE:3.11 | R$ 497.55 bn DRE:3.01 | 22.2% |
| ITUB4 · bank | R$ 45.85 bn DRE:3.09 | R$ 387.12 bn DRE:3.01 | 11.8% |
| BBAS3 · bank | R$ 16.78 bn DRE:3.11 | R$ 319.46 bn DRE:3.01 | 5.3% |
Download any of these DFPs from the CVM, take the income and revenue accounts, divide — you get the same number. Banks use interest income as revenue (what makes sense for a bank), so the reconciliation is sector-aware; the others use sales revenue. Figures are the latest fiscal year.
Why we may differ from another site (and it's not an error)
A difference between two sites usually isn't one being wrong — it's a method choice. We disclose ours:
- Average vs ending capital: ROE and ROIC use average equity/capital (this year + last ÷ 2), not the ending balance — so they're not a naive single-year division.
- Controlling vs consolidated: we state which one each item uses.
- TTM vs annual: quarterly multiples use trailing-twelve-months earnings.
- IFRS 16, goodwill, cash, exceptional tax: each handled explicitly and flagged when it distorts.
Do the tests bite? Yes — real cases the suite has caught
A test that never fails could mean perfect data — or a weak test. These were born from real errors that slipped through, and now fail — only shown for tests that actually exist in the collected suite above:
- I-B16 · banks' P/E = 0: the CVM reports share count sometimes in units, sometimes in thousands (varying by company and year); market cap came out 1000× too small and P/E was zero. I-B16 (price × shares recomputed) caught it — we fixed 661 annual and 2,027 quarterly multiples.
- I-B24 · the vanishing profit: the profit/equity account varies by bank (Itaú 3.09, BB 3.11); if the label search fails, the number vanishes silently and ROE is born wrong. I-B24 makes that hole fail.
- H-B23 · the 800% dividend yield: old B3 dividends carry a price not adjusted for splits; without H-B23 the yield would look absurdly real. The flag keeps the record and warns.
- I-B20 · the missing COTAHIST 2023: on the first deploy one year of prices failed to download silently; the API shipped with ~50k fewer prices and the suite passed, because nothing checked coverage. We added I-B20 — a year without prices now fails the build loudly.
- I-B33 · Oil & Gas at −89.6% in one week: COTAHIST isn't split-adjusted; a 10:1 split read as a −90% weekly return and stayed in the cumulative return forever, with nothing failing (a blind amplitude filter didn't fix it either — it just flipped which direction was wrong). I-B33 (no sector week beyond ±35%, the ruler is the exchange itself) stops the build.
- I-B31 · 364 of 865 sector-index weeks frozen: extending the series back to 2010, a sector with no company carrying a market cap that week turned into a factor of 1.0 — a flat line that the cumulative return read as 'market stood still', and the 'since 2010' return came out fictional (+527%). I-B31 fails on any week repeated to the cent.
- I-E05 · EMBRAER's 2010 P/E priced with 2025 data: the point-in-time price lookup fell forward with no ceiling whenever the current ticker had no price at balance-sheet time (ticker change, share-class migration); the multiple came out priced 15 years into the future, in 22% of annual and quarterly rows, silently. The 45-day window (and I-E05) blocks it: no session inside the deadline, no multiple computed.
- I-E04 · AZUL at R$ 1.67 quadrillion market cap: the balance sheet carried 54.7 trillion shares from the judicial recovery issuance, but the price used was already on the post-reverse-split (1:150,000) basis — two ends measured on different bases, market cap wrong by orders of magnitude. I-E04 (market cap never above Brazil's GDP) fails the build on that absurdity.
How far back each block of data goes
No asterisks here: this is coverage year by year, counted right now. Indicators (ROE, ROIC, margins, growth) come from the filings alone and cover the whole series. Multiples (P/E, P/BV, EV/EBITDA) need a price and a share count — and the share count comes from CVM's reference form, which the further back you go the fewer companies filed in a usable format. The gap between the two columns is a source limit, not an ingestion hole. If you look for an old P/E and do not find it, it is because nobody has it — not because we hid it. The same numbers as JSON: /cobertura.
| Year | Companies with indicators | Companies with multiples |
|---|---|---|
| 2025 | 438 | 290 |
| 2024 | 444 | 296 |
| 2023 | 442 | 298 |
| 2022 | 429 | 295 |
| 2021 | 423 | 296 |
| 2020 | 410 | 274 |
| 2019 | 371 | 164 |
| 2018 | 326 | 164 |
| 2017 | 317 | 162 |
| 2016 | 311 | 152 |
| 2015 | 303 | 143 |
| 2014 | 301 | 139 |
| 2013 | 292 | 130 |
| 2012 | 291 | 111 |
| 2011 | 284 | 97 |
| 2010 | 277 | 34 |
Live coverage
456 companies · 876,076 price points · 8,654 recorded restatements · last refresh 2026-09-27 00:20:28. Full live counts at /saude.
Sources: CVM (open data, ODbL) and B3 (COTAHIST). Not affiliated with B3 or the CVM. Not investment advice.
Numbers on this page are live. Data version 2026-09-27-reapresentacao-agreg · page generated 2026-09-27 06:52 UTC. If this does not match /saude, you are reading a cached copy.