A browser extension for Copart and IAAI lot pages.
Licence: GPL-3.0-only. Published so that anyone installing the extension can audit exactly what it reads and what it sends — see What it sends. Copyleft, deliberately: fork it if you like, but a derivative you ship has to be open under the same terms.
The auction archive the extension queries is a separate, proprietary service. This licence covers the extension source only.
On a Copart lot page, answers the one question the auction site will not: has this exact car been through auction before, and what do cars like it actually sell for?
Free, no account, no quota. Published for the Chrome, Edge and Firefox stores; this file is how to build and run it.
The design rationale lives in a private spec — where this README says "owner decision", that is what it is referring to. The decisions themselves are stated here in full, and each one that could regress is enforced by a test rather than by convention.
Owner decision, 2026-08-27. There is no VIN-removal funnel, no price, no "check my VIN", and no deep link into an order flow — not in the panel, the popup, the options page, or the store listing. Removal is the site's business, bought by the owner of a car; this panel's audience is bidders. The extension's job is to be useful and to put cleaned.vin in front of people who have never heard of it.
The one outbound link is Full report on cleaned.vin →, with no commercial claim.
test/panel.test.ts fails the build if removal copy reappears in any panel state.
This is separate from — and must not be confused with — the server-side removal guard, which is intact and must stay intact: a VIN whose owner paid for removal returns an empty answer that is byte-identical to a genuine miss.
Every field below comes from sanitize() in src/sw.ts, which is the only place a
request body is assembled. Read that function rather than trusting this list — it is
short, and it is the authority.
| Sent | From |
|---|---|
| The VIN | as printed on the lot page, when the auction site shows it |
| The masked VIN | e.g. WDDGF8AB0DR******, when the site hides it. Those visible characters identify a model and a plant, never a car |
| The lot or stock number | of the listing being viewed |
| Year, make, model, trim | as printed on the page, plus the vehicle description from the page address, so multi-word makes resolve |
| An anonymous install token | identifies the installation, not a person |
It is one POST to https://cleaned.vin/api/ext/v1/lot, sent with credentials: 'omit'
so no cookies ride along and it cannot be tied to a site session. Nothing else leaves
the browser: not your browsing history, not page contents, not anything you type, and
no account or payment detail — there is no account.
chrome.storage.local holds four things and none of them are transmitted beyond the
above: the install token, a flag recording that setup was completed, a one-entry
diagnostic the popup reads back, and a dev-only API base override.
The full disclosure, in prose, is at https://cleaned.vin/extension/privacy.
npm install
npm run check # typecheck -> test -> release build -> artifact gates. The gate.
npm run build # a DEV build, if that is what you want in dist/ (see below)
npm run dev # local fixture lot page + a stand-in API on 127.0.0.1:8787Then load dist/ at chrome://extensions → Developer mode → Load unpacked, and open
http://127.0.0.1:8787/lot/55163796/salvage-1997-american-general-h1-tx-dallas.
A RELEASE build. The gate asserts things about the artifact, so it has to end on the artifact it asserted about — it used to finish with a dev build after the checks had already passed, which meant the thing you loaded was never the thing that was inspected.
The consequence is worth stating plainly, because it looks like a bug the first time: a
release build declares copart.com and iaai.com but not the 127.0.0.1 fixture host, so
the local harness at npm run dev does nothing against a release build. Run npm run build
if you want the dev manifest back.
IAAI ships in v1.0. Owner decision 2026-08-28, overriding spec 5.1, which had held it back for its own v1.1 review on the grounds that a declared host a store reviewer never sees exercised is a rejection risk.
That reasoning was sound while the adapter was a guess. It is not one now — it is verified
against a live signed-out IAAI lot page: it reads the printed Stock # rather than the
integer in the URL (which is an internal item id appearing nowhere on the page), takes
year/make/model from the heading because IAAI's URL carries no slug, detects the masked
VIN, and mounts inline in the bidding rail. src/adapters/iaai.ts documents what was
measured; fixtures/iaai-lot.html mirrors the real DOM.
The residual risk was stated and accepted: a second host is a second thing to justify at review, on a new developer account. The store listing must name IAA as well as Copart — a declared host the listing does not mention is its own problem.
scripts/lint-bundle.mjs now asserts the release manifest does declare iaai.com, so the
host cannot silently disappear and leave enabled code running nowhere.
npm run dev reads the production database read-only via the backend repo's
.env.local, so the panel shows genuine data. It never sends a request to copart.com.
To point the service worker at a local backend instead of production, set the override in the extension's service-worker console:
chrome.storage.local.set({ 'cvin.apiBase': 'http://127.0.0.1:3003' })Only https:// and explicit loopback are accepted (src/shared/endpoints.ts).
node scripts/build.mjs # dev: Copart + IAAI + the 127.0.0.1 fixture host
node scripts/build.mjs --release # release: Copart ONLY, minified, no dev hostThe release build strips the loopback host and drops the (dev) name suffix. It keeps
copart.com and iaai.com — both ship in v1.0 (see IAAI above).
1. The content script makes no network requests. A content script is same-origin with
the host page, so it could — omitting host_permissions does not prevent it. Every
request goes through the service worker, and scripts/lint-bundle.mjs greps the built
bundle against a denylist of network primitives and fails the build if one appears.
That gate is a denylist, not a proof, and it is worth being exact about that before the
claim is quoted anywhere: it covers fetch/XMLHttpRequest/WebSocket/EventSource/
sendBeacon/Worker/RTCPeerConnection/import(), plus the two indirections that used
to walk straight past it — an <img>.src assignment and a remote url(...) or @import
inside the shipped stylesheet. A determined author could still reach the network in a way
it does not enumerate. It is the mechanical half of the guarantee; review is the other half.
The same gate asserts the extension sells nothing, and it runs against a release build
because what ships is the only thing worth asserting about. That is not pedantry: a
comment in popup.html explaining the no-funnel decision once put the literal string
$40 VIN-removal funnel inside the packaged artifact, three lines above a claim that no
string in the package quotes a price. A Chrome reviewer greps the artifact, not the repo.
Note also that comments in panel-css.ts ship — it is a template literal, so
minification never reaches inside it.
2. A removed VIN must be indistinguishable from one we never had. Not merely hidden — indistinguishable. Suppression is implemented as "we hold no rows for this VIN", so a removed VIN takes the same code path as the ~93% of lots with no prior sale, and the two are identical by construction rather than by keeping two shapes in sync. An earlier version returned a purpose-built empty body and was a one-request oracle for "this VIN paid for removal" — strictly worse than showing the data. The dev harness honours this too.
3. Never render the wrong car. A lot page is full of other cars — the similar-lots
carousel puts several perfectly valid VINs in the DOM. chooseVin() drops any candidate
whose position-10 model year contradicts the year the URL slug gives us for free, and
renders nothing when nothing agrees. Silence beats a wrong answer.
4. Never automate against Copart. Spec §4.6: the account and IP you flag will be your
own, and once flagged you have poisoned your ability to test. src/adapters/devfixture.ts
and fixtures/lot.html exist so the whole pipeline can be exercised end to end without a
single request reaching an auction site.
src/core/ the site-agnostic loop. Knows of no auction site.
src/adapters/ everything that knows what Copart and IAAI are, plus the bundled config
src/shared/ VIN logic (ported verbatim from the backend), endpoint resolution
src/ui/ the panel
src/pages/ the toolbar popup
test/ node:test + jsdom. Runs the real adapter against the real fixture.
Backend: POST /api/ext/v1/lot in ../auction-history-homepage
(app/api/ext/v1/lot/route.ts + lib/ext-lot.ts). Committed on staging, not
deployed.
i18n (7 locales), the storage cache, telemetry, remote-config fetching, the registration
bridge, and the welcome/options pages. Spec §3.1 marks each of them. The Copart discovery
procedure (§4.6) has not been run — see fixtures/copart/FINDINGS.md.
store-assets/ holds everything a store listing form asks for, so a submission is
paste-and-upload rather than a design task:
| File | Used by |
|---|---|
icon.svg |
Source of truth. icons/{16,48,128}.png are rendered from it |
icon-128x128.png |
Chrome Web Store, Edge Add-ons, Opera, AMO (the packaged icon is the same file) |
logo-300x300.png |
Edge Partner Center store logo |
screenshot-1-prior-sale-1280x800.png |
Every store. The panel on a lot page with a prior sale, an earlier record, a refused bid, the market band and buyer countries |
screenshot-2-signed-out-market-1280x800.png |
Every store. The signed-out path: VIN masked, model-level market band |
screenshot-3-prior-sale-dark-1280x800.png |
Optional. Screenshot 1 in the panel's dark theme |
screenshot-harness.ts |
How the screenshots were made |
The screenshots are rendered, not captured: screenshot-harness.ts mounts the real
render() from src/ui/panel.ts with a hand-written data payload into
fixtures/lot.html (with the fixture's MOCK labels stripped) and headless Chrome
screenshots it at 1280×800. Nothing was fetched from Copart or IAAI, and no real customer
VIN appears — the payload is illustrative. The lot page around the panel is the fixture's
generic layout, not either auction site's branding.
To regenerate after a panel change: bundle the harness with esbuild
(--define:__SHOT__='"hit"' or '"model"'), drop the bundle into a copy of the fixture,
and run Google Chrome --headless=new --window-size=1280,800 --screenshot=out.png file://….