Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

RFP Tracker — find E-Rate Form 470 bids before your competitors do

RFP Tracker is an open-source tool that finds public K-12 and library procurement opportunities, reads the attached documents, and tells you which ones are worth bidding on.

It pulls FCC Form 470 filings from USAC Open Data, downloads the RFPs and attachments those filings point at, extracts what is actually being bought — equipment, services, manufacturers, quantities, deadlines — scores each opportunity against your business profile, and alerts you when a strong match is posted.

Built for vendors selling into the E-Rate programme: network equipment, structured cabling, Wi-Fi, firewalls, managed services and internet access.

Runs on plain Node — no build step, no framework, no database server. Node 22.6+ executes the TypeScript directly and persistence is node:sqlite.

What it does USAC Form 470 + monitored websites → download attachments → extract requirements → score → alert
Built for E-Rate vendors and resellers chasing K-12 and library bids
Documents it reads PDF, DOCX, XLSX, CSV, HTML — detected by magic bytes, not file extension
Scoring Deterministic and fully explainable, tuned to your own business profile
Works offline Yes. Without an API key it uses rule-based extraction; --dry-run costs nothing
Stack Node 22.6+, native TypeScript, node:sqlite, vanilla HTML/CSS/JS

Quick start

npm install
cp .env.example .env          # then add your ANTHROPIC_API_KEY
npm run pipeline -- --limit 25
npm run serve                 # → http://localhost:4000

npm run pipeline runs the whole chain: init → ingest → extract + analyse → alert. Every stage is also runnable on its own.


What each command does

Command What it does
npm run check Self-check: Node version, SQLite, parsers, schema legality, scoring sanity, USAC reachability
npm run test:extract Runs every document extractor against real files (PDF, DOCX, XLSX, CSV, TXT, HTML, stub)
npm run ingest Pulls opportunities from USAC + configured websites
npm run analyze Downloads attachments, extracts text, runs the model, scores
npm run rescore Re-scores stored analyses after you edit the business profile — no network, no API cost
npm run alerts Emits alerts for strong matches; writes data/alerts.json
npm run pipeline All of the above in order
npm run serve Dashboard + JSON API

Useful flags:

npm run ingest  -- --limit 200 --states TX,CA --since 2026-08-01
npm run analyze -- --limit 20 --max-docs 6
npm run analyze -- --dry-run      # download + extract, no model call, no cost
npm run analyze -- --offline      # rule-based extraction, no model call
npm run alerts  -- --threshold 80

How it works

USAC Open Data ─┐
                ├─→ opportunities ─→ documents ─→ extraction ─→ Claude ─→ analysis ─→ score ─→ alerts
Web sources ────┘                                                                        └─→ dashboard

Ingest. USAC's Open Competitive Bidding dataset (jt8s-3q52) is the primary source. It is denormalised — one row per service request — so rows are grouped back into one opportunity per Form 470, carrying every itemised request (function, quantity, unit, min/max capacity) and links to the certified Form 470 PDF and any RFP attachment. Web sources are generic link harvesters: they fetch a listing page and pull out links matching a pattern.

Extraction. Documents are identified by magic bytes, not by file extension — hosts routinely serve HTML error pages from .pdf URLs, and trusting the extension is how a stub becomes a phantom "scanned PDF". PDFs with a text layer are parsed directly; PDFs without one are marked scanned and passed to Claude as native PDF document blocks so it reads them visually. DOCX goes through mammoth, spreadsheets through SheetJS (every sheet, as CSV).

Analysis. One structured-output call per opportunity. The JSON schema in src/lib/ai/schema.ts is enforced by the API, so the response always parses. The prompt is explicit that absent facts must come back as null — quantities and manufacturers are never inferred.

Scoring. Deterministic and fully explainable: technology fit (35), lines carried (15), geography (10), organization type (10), services (10), billing method (8), deal size (7), timing (5), minus penalties for exclusions and low-confidence extractions. Every point is traceable to a rule, and the dashboard shows the whole breakdown.

Alerts. Anything at or above ALERT_THRESHOLD fires once per channel (console, webhook), tracked in the alerts table so nothing repeats. data/alerts.json is rewritten every run for polling.


Configure it for your business

Everything scoring depends on lives in src/lib/config.ts:

export const businessProfile: BusinessProfile = {
  coreTechnologies: ["network switching", "wireless", "firewall", ...],
  manufacturers:    ["Cisco", "Meraki", "Aruba", ...],
  priorityStates:   ["TX", "CA", "FL", ...],
  minDealSizeUsd:   10_000,
  ...
};

The shipped profile assumes a K-12 / library network-infrastructure VAR (switching, Wi-Fi, firewall, cabling, UPS). Edit it to match your actual business, then npm run rescore — no re-ingest, no API cost.

Add monitored sites to webSources in the same file.


API

Endpoint Purpose
GET /api/opportunities Search + filter. q, state, type, billing, technology, minScore, openOnly, sort, limit, offset
GET /api/opportunities/:id Full detail: analysis, score breakdown, documents, contact
GET /api/facets Filter values with counts, plus corpus stats
GET /api/profile Active business profile and threshold
GET /api/health Liveness
curl 'localhost:4000/api/opportunities?q=switch&state=TX&minScore=70'


Frequently asked questions

What is FCC Form 470?

A Form 470 is the filing a US school or library must post before buying E-Rate-eligible products or services. It opens a mandatory 28-day competitive bidding window, and it is public. For a vendor it is the earliest reliable signal that a buyer is about to spend money, which is why this tool is built around it rather than around commercial bid-aggregator listings.

Where does the data come from?

USAC Open Data — the Open Competitive Bidding dataset (jt8s-3q52), which is free and public. No scraping, no login, no paid feed. A Socrata app token is optional and only raises the rate limit. The tool can also monitor regional education-service-centre and procurement pages for RFPs that never make it into the dataset.

Does it read the actual RFP documents, or just the filing metadata?

It reads the documents. A Form 470 tells you almost nothing on its own — the requirement lives in an attached PDF or spreadsheet. The tool downloads every attachment, identifies it by magic bytes rather than by file extension — hosts routinely serve an HTML error page from a .pdf URL, and trusting the extension is how a stub becomes a phantom "scanned document" — and extracts the equipment, manufacturers, quantities and deadlines from it.

How does the scoring work, and can I change it?

Scoring is deterministic and fully explainable: technology fit, lines carried, geography, organisation type, services, and billing method, each contributing a stated number of points to a 0-100 total. Every score can be traced back to the rule that produced it. It is tuned to your business in src/lib/config.ts, and npm run rescore re-scores everything already in the database with no network calls and no API cost.

What is BEAR vs SPI, and why does the tool detect it?

They are the two E-Rate reimbursement routes. BEAR means the applicant pays the vendor in full and claims the discount back afterwards; SPI means the vendor invoices USAC directly for the discounted portion. Which one a filing specifies materially changes a vendor's cash position, so the tool extracts it and it is a scoring input.

Does it cost anything to run?

The USAC data is free. The only paid component is the AI summarisation, and it is behind an explicit flag: --dry-run downloads and extracts without calling any model, --offline uses rule-based extraction, and --limit caps every run. You can operate the whole pipeline with no API key at all.

Can it handle scanned PDFs?

Partly, and honestly. A PDF with no text layer is detected as scanned and passed to the model as a native document block so it can be read visually. Without an API key, a scanned document yields nothing and says so rather than reporting zero values as though they were findings.

Known limitations

USAC's document host is currently serving placeholders. Every object on publicdata.usac.org — including documents search engines have indexed — returns the same 144-byte HTML meta-refresh stub instead of the file. This is an outage on their side, not a bug here. The tracker detects these explicitly (extraction_mode = "placeholder"), does not cache them, and analyses those opportunities from posting metadata alone at reduced confidence. When USAC restores the host, the next npm run analyze picks up the real files with no code change.

The USAC API's own data remains rich and is what the current analyses are built from — itemised service requests with quantities, capacities, and the applicant's own narrative description.

Without an ANTHROPIC_API_KEY, npm run analyze falls back to a rule-based extractor. It reads the source's structured fields and applies keyword rules, and marks every row model = "rule-based-fallback" with low confidence so those rows are never confused with model output. Set the key and re-run to upgrade them.

Web source monitors are generic. They harvest links from a listing page and will find nothing on sites that render listings via client-side JavaScript; those need a per-site adapter. The two shipped example URLs are unverified placeholders — replace them with the portals you actually track.


The Ralph loop

ralph/ holds an autonomous build loop (technique): the same prompt is fed to Claude repeatedly, one backlog item per iteration, until completion criteria are met.

MAX_ITERATIONS=10 ./ralph/loop.sh
  • ralph/PROMPT.md — the loop prompt and completion criteria
  • ralph/fix_plan.md — the backlog; one item per iteration, ticked only with proof
  • ralph/JOURNAL.md — what each iteration did

Layout

src/
  lib/
    config.ts          business profile + monitored sources  ← edit this
    db.ts store.ts     schema and persistence
    queries.ts         dashboard search/facets
    score.ts           deterministic scoring
    alerts.ts          alert channels
    http.ts            fetch with retry + backoff
    sources/           usac.ts, web.ts
    parse/extract.ts   magic-byte sniffing + all extractors
    ai/                schema.ts, analyze.ts, fallback.ts
  scripts/             init-db, ingest, analyze, rescore, alerts, pipeline, selfcheck, test-extract
  server.ts            JSON API + static host
public/index.html      dashboard (zero dependencies)
data/                  sqlite db, document cache, alerts feed (gitignored)

About

Find E-Rate FCC Form 470 bids before your competitors. Pulls K-12 and library procurement filings from USAC Open Data, reads the attached RFPs, extracts equipment and deadlines, scores each opportunity against your business, and alerts on strong matches. Plain Node, no build step.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages