Used-car marketplaces win or lose on trust. A listing that only shows the seller's typed year and model invites mismatch fraud: the ad says one car, the VIN encodes another. Paid history reports cover title brands and event trails, but they are the wrong default for every browse session. What you can embed cheaply is a pre-purchase VIN checklist: validate the VIN, decode manufacturer identity, compare it to the listing, and state clearly what free decode cannot prove.
This post sketches that pattern, with TypeScript shapes you can drop behind a form.
What the checklist is for
Buyers usually ask one bundled question: "Is this VIN okay?" Split it into jobs your UI can actually finish:
- Is the string a legal 17-character VIN (no I/O/Q; check digit when applicable)?
- Does free decode return a coherent Make / Model / ModelYear?
- Do those attributes match what the listing claims?
- Has the buyer been told that decode is not title history?
- Are the next steps (official recall check, paid history, inspection) obvious?
Jobs 1-3 are free-decode territory. Jobs 4-5 are copy and navigation. Mixing them up ships a green "verified" badge that only means "the API returned a year."
Free decode vs history: say it once, early
Put a single disclaimer near the checklist, not after the user has already treated empty accident fields as "all clear":
This check reads manufacturer attributes from public VIN decode data. It is not a title, salvage, flood, odometer, lien, or accident history report.
One visible sentence is enough. Five repeats feel like legal wallpaper; omitting it feels like overclaim.
Checklist UX pattern
Think of ordered steps with explicit states: pass, fail, unknown, manual.
| Step | Auto when possible | Fail looks like | Unknown looks like |
|---|---|---|---|
| Format | length, charset | "Not a 17-char VIN" | n/a |
| Check digit | ISO / 49 CFR math | "Likely typo" | Skip where digit rules differ |
| Decode identity | vPIC Make/Model/Year | "Did not decode" | Partial row with blanks |
| Listing match | compare seller fields | Year or model mismatch | Seller left year blank |
| Scope reminder | static copy | n/a | Always show |
| Next actions | links | n/a | History + inspection + recalls |
Visual rules that keep trust:
- Mismatch is red. If the listing says 2018 and the VIN decodes 2016, do not soften it to "please review."
- Blank is gray, not green. Missing PlantCity is not safety clearance.
- No fake score. A 0-100 "VIN health" meter built only from decode fields trains the wrong habit.
- Sellers cannot dismiss identity fails. Allow photo appeals of the dash VIN; do not hide conflicts behind a checkbox.
TypeScript: checklist model
type StepStatus = "pass" | "fail" | "unknown" | "manual";
type ChecklistStep = {
id: string;
label: string;
status: StepStatus;
detail?: string;
};
type ListingClaims = { make?: string; model?: string; year?: number };
type DecodeIdentity = {
make: string | null;
model: string | null;
year: number | null;
notes: string | null;
};
const VIN_RE = /^[A-HJ-NPR-Z0-9]{17}$/;
function norm(s: string | null | undefined) {
return (s ?? "").trim().toUpperCase().replace(/\s+/g, " ");
}
export function buildChecklist(
rawVin: string,
listing: ListingClaims,
decoded: DecodeIdentity | null,
checkDigitOk: boolean | null
): ChecklistStep[] {
const vin = rawVin.trim().toUpperCase();
const steps: ChecklistStep[] = [];
steps.push({
id: "format",
label: "VIN format (17 chars, no I/O/Q)",
status: VIN_RE.test(vin) ? "pass" : "fail",
detail: VIN_RE.test(vin) ? vin : "Fix the VIN before decode",
});
if (checkDigitOk != null) {
steps.push({
id: "checkDigit",
label: "Check digit",
status: checkDigitOk ? "pass" : "fail",
detail: checkDigitOk ? undefined : "Position 9 does not match calculated digit",
});
}
if (!decoded) {
steps.push({
id: "decode",
label: "Manufacturer decode",
status: "fail",
detail: "No identity returned",
});
return steps;
}
const hasIdentity = Boolean(decoded.make && decoded.year);
steps.push({
id: "decode",
label: "Manufacturer decode",
status: hasIdentity ? "pass" : "unknown",
detail: hasIdentity
? `${decoded.make} ${decoded.model ?? ""} ${decoded.year}`.trim()
: decoded.notes ?? "Incomplete decode",
});
const yearMatch =
listing.year == null || decoded.year == null
? "unknown"
: listing.year === decoded.year
? "pass"
: "fail";
steps.push({
id: "yearMatch",
label: "Listing year matches decode",
status: yearMatch,
detail:
yearMatch === "fail"
? `Listing ${listing.year} vs decode ${decoded.year}`
: yearMatch === "unknown"
? "Need both listing year and decoded year"
: undefined,
});
const makeClaim = norm(listing.make);
const makeDec = norm(decoded.make);
const makeMatch =
!makeClaim || !makeDec
? "unknown"
: makeDec.includes(makeClaim) || makeClaim.includes(makeDec)
? "pass"
: "fail";
steps.push({
id: "makeMatch",
label: "Listing make matches decode",
status: makeMatch,
detail: makeMatch === "fail" ? `Listing ${listing.make} vs decode ${decoded.make}` : undefined,
});
steps.push({
id: "scope",
label: "History and title brands",
status: "manual",
detail: "Not included in free decode - use a history report if you need brands or events",
});
steps.push({
id: "inspection",
label: "Pre-purchase inspection",
status: "manual",
detail: "Mechanical and flood/body checks need a human, not an API",
});
return steps;
}
Wire decoded from your vPIC client. Keep year matching strict; make matching can use a small synonym map for "VW" vs "Volkswagen."
Where to put it on the page
Good placements: seller compose (block format fails; warn on identity mismatch); buyer listing detail near price; offer/contact modal before personal data is shared.
Bad placements: blog footer only; a paywall when you only ran free decode; a toast that vanishes in three seconds.
Copy snippets that stay honest
Use: "Manufacturer identity from public decode"; "Listing does not match decode - ask the seller to explain"; "Title brands and accident records need a separate history report."
Avoid: "Vehicle history verified"; "No issues found" when you never queried issues; "Clean VIN" as a synonym for "check digit passed."
Disclosure
I maintain VIN Lookup, a free VIN decode for manufacturer attributes. It is a useful building block for checklist step "decode identity"; it is not a marketplace history product and not a substitute for NMVTIS-aware reports or a pre-purchase inspection.
Ship the small promise
Embed the checklist so buyers see format, identity, listing match, and a clear handoff to history and inspection. That catches listing mistakes without pretending free decode is a full pre-purchase file.
If you run a marketplace, measure year/make mismatch rates and whether sellers fix or abandon. The checklist works when conflicts are visible instead of silent.
Top comments (0)