Inspiration
I run Detox Earth Initiative, a youth-led nonprofit that audits the chemicals used in schools. This year, I audited my own district's products by hand. I pulled the Safety Data Sheet for each one, read the ingredient section, and looked up every CAS number against hazard lists. It took weeks to get through 39 products. I found a reproductive toxicant in gym floor finishes, nonylphenol ethoxylates in a tile cleaner, and preservatives I had never heard of in product after product. The district had 116 products. Most districts have no one to do this at all. Every product already comes with an SDS that lists its ingredients. Nobody has time to accurately read it, so I built a tool that does.
What it does
A district uploads its SDS PDFs. The tool:
- Finds the ingredient section (Section 3) in each sheet, including old formats that put it in Section 2
- Pulls out every ingredient and its CAS number
- Checks each CAS against California Prop 65, the EU SVHC Candidate List, and the EU Endocrine Disruptor Lists I to III
- Flags products that hide an ingredient's identity as a trade secret
- Reads scanned PDFs with OCR
- Gives each product a level (HIGH, MEDIUM, UNDISCLOSED, LOW, or UNREADABLE), with the rule that triggered it, the list, the hazard in plain English, and a link to the source
LOW means no list match, not proven safe. The app says that on every results page.
How we built it
The tool is written in Python, with a Streamlit interface.
Extraction. pdfplumber reads each PDF. A regex section splitter finds the composition section by its title, not its number, since some manufacturers put ingredients in Section 2. A row parser pulls the name, CAS number, and concentration range from each line.
CAS validation. Every CAS number has a built-in check digit. For a CAS number with digits \(d_n \dots d_2 d_1\) and check digit \(c\):
$$c = \left( \sum_{i=1}^{n} i \cdot d_i \right) \bmod 10$$
The parser rejects any number that fails. This catches manufacturer typos and OCR misreads before they can become false matches.
Hazard database. I downloaded the four lists into a SQLite database. Many listings name a group, like "Lead and lead compounds," or one form of a chemical, while products contain a salt or isomer. I mapped those by hand in a cas_groups.csv file, with a cited source for every row.
Scoring. The levels come from fixed rules. I wrote them down and froze them with a git tag before validation, so I could not tune the rules to make the results look better.
OCR. Many districts scan their SDS binders. Scanned files fall back to OCR with PyMuPDF and RapidOCR, and the CAS checksum guards against misread digits.
Challenges we ran into
- SDSs are not standardized in practice. I found ingredient lists in Section 2 instead of Section 3, translated headers, Spanish sheets, CAS numbers printed without dashes, and fields split across lines.
- "C12-16" is not a percentage. Surfactant names include carbon-chain lengths. My parser read "C12-16" as 12 to 16 percent and invented a hidden ingredient. A one-character regex fix solved it.
- Trade-secret wording is vague by law. OSHA requires the phrase "the specific chemical identity and/or exact percentage... has been withheld." The "and/or" doesn't say which one was hidden. My first version flagged almost every product. I rewrote the rule to flag only missing identities.
- The lists name one form of a chemical. Arsenic, glyphosate salts, and fluazifop-P-butyl were all missed until I mapped the related forms by hand.
- Scans. At one district, 8 of 12 files were scans. Before OCR, the tool could not read any of them.
Accomplishments that we're proud of
- It found something a careful reader would miss. In a bathroom disinfectant from another district, the tool flagged methoxyacetic acid, an EU-listed reproductive toxicant, at under half a percent on row 11 of Section 3. It took seconds.
- I froze the rules before testing. The scoring rules were tagged in git before validation, so the results could not be tuned to look better.
- It works on files it has never seen. On 10 randomly chosen products nobody had reviewed, it agreed with a manual review on 10 of 10.
- It held up outside my district. I tested 35 SDSs from districts in California and Connecticut and a college in New York. Every bug I found, I fixed.
- Scanned PDFs are no longer a dead end. At one district, 8 of 12 files were scans. With OCR, the tool now reads all 8.
- It gives the same answer for the same product across districts. Ranger Pro, Roundup PROMAX, and Spartan's Clean By Peroxy got matching levels from two different districts' PDFs.
- Every flag can be checked. Each result shows the list, the hazard in plain English, and a link to the official source.
What we learned
- Regulatory lists lag the science. In my district, 11 products contain preservatives and plasticizers that research links to endocrine disruption, and no list I used flags them. The tool can only find what lists already know.
- Validation matters more than features. Freezing the rules before testing, and testing on files I had never seen, told me more than any feature I built.
- "Green" labels don't replace checking ingredients. A cleaner labeled "97% naturally derived" listed no ingredients I could check.
What's next for ToxiCheck: SDS Chemical Audit
- Automatic mapping of salts, esters, and isomers using PubChem
- Safer replacement suggestions for flagged products
- Expand datasets to mark more instances of EDCs
- Better support for non-English SDSs
- A pilot with a school district
Log in or sign up for Devpost to join the conversation.