Skip to content
cleanvinusaPublic

About

Browser extension showing prior auction sale records and market comps on Copart and IAAI lot pages.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

cleaned.vin — Auction History

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.

The extension sells nothing

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.

What it sends

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.

Running it

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:8787

Then 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.

What npm run check leaves in dist/, and why it surprises you once

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

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).

Builds

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 host

The 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).

Four invariants

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.

Layout

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.

Not built yet

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

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://….

About

Browser extension showing prior auction sale records and market comps on Copart and IAAI lot pages.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages