Car Passport
A vehicle history archive for Pakistan's used car market: it links repeat listings of the same car and flags where their claims conflict, such as mileage that goes down between listings.
- Role
- Founder, engineer
- Period
- 2026 to present
- Stack
- Python, SQLite, perceptual hashing (pHash)
- Status
- In progress
- distinct listings archived
- 100,918
- cars linked across two or more listings
- 636
- mileage claim conflicts flagged for review
- 124
Context
Pakistan’s used car market runs on trust. There is no widely used vehicle history report, so a buyer has little way to check whether the mileage is real, how often the car has changed hands, or whether the seller’s story matches the car’s past. Yet many cars pass through the same public marketplace more than once, and every appearance leaves a record.
Car Passport turns those scattered appearances into a history.
The problem
The idea is simple: look up a car, see every time it has appeared on the market, and see where its claims contradict each other. Mileage that goes down between listings. A car relisted again and again in a short window. Details that don’t line up.
The engineering problem is that nothing reliably says two listings are the same car.
What I did
A crawler and archive, written in Python with SQLite.
Finding every listing. Instead of slicing the marketplace by make, model or city, I enumerated the whole site from its sitemaps. The census in August 2026 found about 77,500 listing URLs, far fewer than I expected, which made a full crawl the right choice. Each listing’s last-modified date in the sitemap becomes its approximate listing date, which gives the archive a time dimension and makes incremental re-crawls cheap.
Crawling responsibly. The crawler follows the site’s robots.txt: no JSON endpoints, no sort-order crawling, and no seller phone numbers, ever. Listings are treated as disposable input. The asset is the history that accumulates across them.
Linking listings to cars. The obvious key, the number plate, failed: plates were readable in only 0.4% of listings. So photos became the primary signal.
- The first 4 photos of each listing are reduced to a 64-bit perceptual hash (pHash), a fingerprint that barely changes when an image is resized, recompressed or lightly cropped. Two photos match when their hashes differ by 8 bits or fewer.
- pHash ignores colour, so it can’t tell a white car from a silver one shot at the same angle. Each photo therefore also gets a 42-bit colour hash, which must agree too.
- One matching photo is not enough. At least half of the smaller gallery has to match, rising to three quarters when both cars show under 1,000 km, because new-car listings often reuse the manufacturer’s press photos.
- Any hash that appears in more than 5 listings is treated as site boilerplate (a dealer logo, a stock image) and ignored.
Key decisions and tradeoffs
Split, not merge
A wrong link is worse than a missed one: merging two different cars invents a fake history and could accuse an honest seller, while a missed link only leaves a history incomplete. So a new listing is tried against existing cars in order of evidence strength (exact plate, then photo reuse, then fuzzy plate), and every candidate link must also pass a compatibility check. The link is blocked if:
- the make or model differ;
- the model year differs by more than one;
- the asking prices differ by more than 40%;
- both cars are nearly new but listed in different cities;
- with no plate match, the sellers, the cities and the mileage (by more than 20%) all differ.
When an exact plate match disagrees with the car’s details, the plate is treated as junk rather than as proof. When nothing passes, the listing becomes a new car. A merge needs positive evidence and no contradiction.
A lesson about naming
The flag was first called “odometer rollback”. I renamed it “mileage claim conflict”. A listing’s mileage is what the seller typed, not a reading from the car, so two listings disagreeing proves that the claims conflict, not that anyone turned an odometer back. Getting the name right matters in a product that makes statements about real people’s cars: the system should claim exactly what its evidence proves.
Lessons from running it
- Commit per row, not per pass. The crawl runs for days on a laptop. Committing every row means a reboot loses nothing.
- Never
SELECT *. Each snapshot keeps the raw page, around 100 to 200 KB, so a careless query ran the machine out of memory. Every query names its columns. - Know your locks. Relinking holds a write lock for its whole run, so it must never overlap the crawl.
Results (as of October 2026)
- 100,918 distinct listings archived, across 103,473 snapshots.
- 204,144 photos hashed so far, across 53,632 listings.
- 636 cars linked across two or more listings.
- 124 mileage claim conflicts flagged for review, where a later listing of the same car states lower mileage than an earlier one.
What’s next
- Finish hashing the archive and keep the crawl running on a schedule, so histories grow on their own.
- Review the flagged conflicts by hand to measure how often a flag reflects a real problem.
- A free, shareable report as the first product, with dealers and then lenders as the paying customers later.
- No price prediction until there is real data on what cars actually sold for.
What I would do differently
I’d count the corpus and sample the data before designing anything. I assumed the site was far larger than it was, and I designed around number plates before discovering that only 0.4% of listings show a readable one. A day spent measuring first would have put photo matching at the centre of the design from the start.