Inspiration
On August 19, 2026 the Illinois Tollway board approved the largest toll increase in state history. The common passenger toll goes from 75 cents to $1.20 in January 2027. From 2029, tolls rise with inflation automatically, with no public hearing required. The money funds a $26.5 billion, 15-year capital program.
The 2027 increase is law. What happens from 2029 depends on what that capital program actually costs.
Here is the gap that started this. Public agencies are effectively required to award construction contracts to the lowest responsible bidder. At award time the agency knows what each bidder says the work will cost. It does not know what that bidder's past bids became. Contracts grow after award through change orders and extra work orders, and some contractors' awards grow far more than others' on comparable work. A contractor whose awards reliably grow nine percent on bridge work is not the low bidder on bridge work. The agency pays that premium invisibly, at scale, on every letting.
The Tollway publishes both halves of the answer and has never joined them. The Construction Contract Tracker exposes, per contract, the Award Amount and the Contract Authorized Amount, which is the award plus every approved increase. The board resolution archive, posted under the Open Meetings Act, states the reason for every increase in plain language. The gap between award and authorized is the overrun. The resolutions are its explanation. All of it is public. Nobody has read it as one dataset.
Then WebMCP changed the question I was asking. If an agent can now act directly on a page, the interesting problem is not what a procurement page lets it do. It is what the page refuses to let it do, permanently, in a way anyone can verify.
What it does
Track Record is a pre-award review surface. An agent arrives, reads the boundary it is operating inside, pulls each bidder's documented award-to-authorized growth history, retrieves the verbatim board-resolution reasons behind the largest increases, and stages a review for a human.
Then it stops. It cannot recommend a bidder. It cannot award a contract. It cannot publish anything until a person grants it one scoped, expiring permission to publish exactly one memo.
The tagline is the whole thesis: the low bid isn't the price.
How WebMCP is used
WebMCP is the product boundary here, not decoration. The agent never scrapes cards, never infers meaning from the DOM, and never guesses which text on the page is authoritative. Four typed tools are registered on document.modelContext, with a navigator.modelContext fallback:
inspect_review_boundaryreturns the purpose, the dataset, the refusal threshold, and the explicit list of allowed and forbidden actions.research_contractor_historyreturns structured award-to-authorized evidence, and refuses to compute growth from fewer than three comparable contracts.get_cost_growth_evidencereturns cited increases with verbatim stated reasons.stage_preaward_reviewputs the exact proposed scope on screen, publishes nothing, and tells the agent to stop for a human.
The most important part of the interface is a thing that is not there. recommend_bidder and award_contract are never registered, under any condition. The page names them on screen as absent, and getTools() proves it.
A fifth tool, publish_authorized_memo, does not exist at page load either. A human creates it, for one staged review, by granting one memo. The grant mints a random TR-AUTH-... identifier, pins it into the tool schema as a const, expires after ten minutes, and removes itself through its own AbortController on use, revocation, expiry, or scope change. After the agent publishes, the live capability map redraws and shows four tools again.
Annotations carry operational meaning rather than prose. Evidence tools set readOnlyHint: true. get_cost_growth_evidence also sets untrustedContentHint: true, because quoted resolution language is source data written by the agency and must never be read as an instruction. Staging and publishing set readOnlyHint: false because they change visible or persisted state.
Every schema sets additionalProperties: false. That constraint matters more when the caller is an agent than when it is a person. An over-parameterized tool is an open invitation to attach unrelated agent-held context: other conversation content, identity details, browsing state, credentials. These tools accept only the fields the declared operation needs. The dynamically registered publish tool accepts exactly authorization_id and purpose. It has no field for a winner, a selection, a ranking, or an award.
How I built it
Two interfaces hold the whole system together, and nothing in the application imports a concrete class.
Repository is implemented by both SQLite and BigQuery. The growth metric lives in one shared module, so both backends compute identical numbers. ModelProvider is implemented by a mock, the Gemini API, Vertex AI, and Google ADK. app/config.py is the only place any environment variable is read. That is why the same prompt, the same tools, and the same tests run on a laptop with no credentials and on Cloud Run against BigQuery and Vertex.
The metric, per vendor and work type: growth_rate is dollar-weighted, (sum authorized - sum award) / sum award. median_growth is the median of per-contract growth. Both are computed and both are shown. When one large contract dominates a vendor's history the two diverge, and that divergence is information the memo displays rather than hides. They are never blended into one number. Blank, absent and non-numeric amounts parse to NULL, never 0, because zero and unknown mean different things in a growth calculation.
Cause clustering is deterministic keyword matching over the verbatim stated reason, not a model call. A reader can see exactly why the memo says what it says.
Four Flask endpoints back the tools and reuse that same deterministic repository. Every WebMCP response carries a full defensive header suite: Origin-Agent-Cluster: ?1, Permissions-Policy: tools=(self), a strict nonce-based CSP, plus frame, sniffing, referrer, cross-origin and HSTS headers. The publish endpoint validates the authorization shape, enforces one-use server side, caps request size, checks bidder scope and positive numeric bids, and recursively rejects decision-shaped keys anywhere in the payload. Untrusted resolution quotes are scanned for injection patterns and returned as inert data. A live safety panel reads the headers back in the browser and marks each one verified.
Verification is offline and deterministic: 75 Python tests, 29 of them WebMCP-specific, plus a Node DOM harness that walks the capability lifecycle. They cover native registration, annotations, schema constraints, deliberate absence, one-use consumption, manual and scope-change revocation, the nonce-to-CSP match, injection scanning, the request size limit, and a complete manual-mode flow in which every endpoint fails.
There is no multi-agent orchestration, no user accounts, no search UI, no scoring taxonomy, no websockets, no build step, no dark mode. Each was considered and cut. Discipline here is the absence of things.
Challenges
The data wall. The board resolution archive is reachable and real, and 2,052 documents are catalogued from it, back through 2013 and forward to the August 19, 2026 meeting. The Construction Contract Tracker refuses connections from Google Cloud. Not a 403, a connection failure. Award Amount and Contract Authorized Amount live only there, and growth is award-to-authorized. Without the tracker there are no real award amounts, and this project does not invent numbers.
So the deployed demonstration runs on a fictional dataset that renders a SAMPLE DATA badge on every screen it backs. All six contractor names are invented. A real contractor name never appears beside an invented number, anywhere. The ingestion path prints a gate-style export request instead of guessing. Drop a CSV export at data/tracker_export.csv, run one script, and the same deployed code serves real figures with the badge gone. Choosing a badged fiction over a plausible fabrication was the right call and it cost the demo something real.
A standard mid-flight. The interface sits on document.modelContext in the August 26, 2026 draft, and on navigator.modelContext in earlier builds and the community polyfill. The registration layer checks both and falls back to a clearly labelled local mirror, so the full lifecycle is still judgeable in a browser with no native support.
Proving a negative. Claiming a tool does not exist is easy. Showing it is harder. The live capability map redraws from getTools() after every toolchange event, and a receipts log records registration, registration failures, evidence reads, insufficient-history refusals, staging, human authorization, publication, and teardown. That turns an invisible permission transition into something a judge can watch and audit.
What I learned
Tool design is permission design. Every field you add to a schema is authority you are handing over, and additionalProperties: false is a security control, not a linting preference.
The refusal turned out to matter as much as the calculation. Fewer than three comparable prior contracts returns the words insufficient history and no percentage, enforced inside the tool and asserted by the most important test file in the suite. Thin history cannot be laundered into a confident-looking procurement signal.
And the pattern generalizes past procurement. Any high-consequence workflow can separate broad read authority from a narrow, visible, expiring human grant: clinical chart review before an order, financial evidence review before a transfer, compliance review before a filing. The tool registry becomes a control plane the agent can inspect and a human can audit.
What's next
Load a real tracker export and drop the sample badge. Backtest across more lettings and publish the error band whatever it says. Extend the same award-to-authorized join to other agencies that publish both halves and have never connected them.
Honest limitations
Growth is computed from the agency's own published award and authorized amounts. Stated reasons are quoted verbatim from board resolutions, which are written to obtain approval and reflect the agency's own framing. Increases absorbed within contingency or below board approval thresholds never appear in the public record and are not counted here.
Track Record was built for this challenge on 2026-08-31, both the base pre-award app and the WebMCP layer on top of it. Nothing was backdated and no commit history was rewritten. The WebMCP-specific code is every commit after 8a22420, itemized in docs/WEBMCP_WHATS_NEW.md.
Track Record supplies a missing variable. It is not a recommendation. Every memo ends the same way: a human decides from here.
Log in or sign up for Devpost to join the conversation.