About Decode VIN
nitrovin is an offline VIN decoder for developers and vehicle-data workflows. It embeds a month of NHTSA vPIC data and provides matching decode behavior through CLI, HTTP, libraries, and MCP.
Key features
- 01Offline VIN decoding with embedded vPIC data
- 02Decode partial and mistyped VINs
- 03Text, JSON, and detailed output shapes
- 04Translate and list vehicle vocabularies
- 05CLI, HTTP, stdio, Go, and C interfaces
Use cases
- 01Decode VINs without network access
- 02Process VINs in command-line batches
- 03Look up vehicle details from an MCP client
MCP support
Provides the `decode_vin`, `translate`, and `list` tools through stdio MCP, with a Streamable HTTP option at `/mcp`.
Configuration
Local server config is the same for every client. Copy it into your client’s MCP config file.
docker run -i --rm ghcr.io/joshuasprow/nitrovin:v0.3.7Local servers need a runtime on your machine (e.g. Node.js 18+). Config file locations differ by client; see your client’s docs.
ghcr.io/joshuasprow/nitrovin:v0.3.7stdioTools
Local servers expose tools only once running on your machine, and the maintainer has not provided a tool list.
Details
From the project READMEnitrovin
An offline VIN decoder that answers like NHTSA's
vPIC. It is a Go port of spVinDecode, the
decoding procedure NHTSA publishes inside its monthly vPIC database, and it
ships as one binary with a month of vPIC data inside. No network, no
database, no API key.
Every data month is checked against NHTSA's own procedure running in Postgres before it ships: every row of every VIN in an 18,318-VIN corpus must match, byte for byte, or the month does not ship (what parity means).
$ nitrovin decode -o text 1HGCM82633A004352 | grep -E '^(VIN|Make|Manufacturer Name|Model|Model Year|Body Class|Plant City|Error Code) '
VIN 1HGCM82633A004352
Body Class Coupe
Make HONDA
Manufacturer Name AMERICAN HONDA MOTOR CO., INC.
Model Accord
Model Year 2003
Plant City MARYSVILLE
Error Code 0
Those are 7 of the 43 elements this VIN decodes to (--shape nitrovin
lists them).
The same decode is reachable six ways: a CLI, an HTTP server, JSON lines
over stdio, an MCP server for AI assistants, a Go module and a C library.
nitrovin.app runs the latest release's server for
anyone: the form, the /v1 API (CORS open) and MCP at
https://nitrovin.app/mcp, rate-limited per IP.
Status: v0.x prereleases. v0 means the release pipeline is not yet
signed off, not that the API may change: the contract is frozen at /v1.
v1.0.0 waits on the gates in
ADR 0007.
nitrovin is not affiliated with or endorsed by NHTSA.
- Install
- Use it: CLI, HTTP, stdio, MCP, Go, C
- Verify a release
- The data
- Vocabularies: translate your names for vehicles and part types
- Parity
- Known limits
- Performance
- Contributing
- Credits and licence
Install
Every release embeds one vPIC data month; nitrovin --version names it.
| Channel | Command | Platforms |
|---|---|---|
| Homebrew | brew install joshuasprow/tap/nitrovin | macOS, Linux |
| Scoop | scoop bucket add joshuasprow https://github.com/joshuasprow/scoop-bucket then scoop install nitrovin | Windows amd64, arm64 |
| winget | JoshuaSprow.Nitrovin, not yet submitted (#78) | Windows |
| Docker | docker run --rm -p 8080:8080 ghcr.io/joshuasprow/nitrovin | linux/amd64, linux/arm64 |
| Go 1.27+ | go install github.com/joshuasprow/nitrovin/cmd/nitrovin@latest | anywhere Go builds |
| Archives | GitHub Releases: nitrovin_<version>_<os>_<arch>.tar.gz (.zip on Windows) | linux, darwin, windows × amd64, arm64 |
| Linux packages | the same releases: sudo apt install ./nitrovin_<version>_linux_<arch>.deb, sudo dnf install ./nitrovin_<version>_linux_<arch>.rpm, sudo apk add --allow-untrusted ./nitrovin_<version>_linux_<arch>.apk | linux amd64, arm64 |
| C library | the same releases: libnitrovin_<version>_<os>_<arch> | linux, darwin, windows × amd64, arm64 |
$ nitrovin --version
nitrovin v0.1.2 (data 2026_10)
The binary is about 70 MB, most of it the data; an archive is about 21 MB.
The Docker image runs nitrovin serve --addr :8080 by default, as a
non-root user, and takes any other command in its place
(docker run --rm ghcr.io/joshuasprow/nitrovin decode 1HGCM82633A004352).
Its tags are vX.Y.Z, vX.Y, vX, latest and data-<YYYY_MM>.
The Linux packages install /usr/bin/nitrovin and put LICENSE and
NOTICE in /usr/share/doc/nitrovin; removing the package removes all
three. They are release assets, not a package repository, so nothing signs
them for apt, dnf or apk (hence --allow-untrusted): verify one with
cosign before you install it,
TAG=v0.1.5
cosign verify-blob --bundle nitrovin_${TAG#v}_linux_amd64.deb.sigstore.json \
--certificate-identity https://github.com/joshuasprow/nitrovin/.github/workflows/release.yml@refs/tags/$TAG \
--certificate-oidc-issuer https://token.actions.githubusercontent.com nitrovin_${TAG#v}_linux_amd64.deb
or check it against checksums.txt as Verify a release
shows.
The binaries are not notarized by Apple. Homebrew removes the quarantine flag for you. A macOS binary downloaded with a browser is killed on launch until you remove it:
xattr -d com.apple.quarantine nitrovin
Use it
Every surface takes the same request and returns the same decode (the API contract):
| Request field | Default | Meaning |
|---|---|---|
vin | required | 1–17 characters; partial (*) and mistyped VINs decode too |
model_year | none | a model year you already know, a hint |
shape | vpic | vpic: vPIC's DecodeVinValues keys (Make, BodyClass); nitrovin: keyed by variable name, with element and attribute ids |
full | false | add items, every row NHTSA's procedure emits, with its source and pattern |
now | the clock | the instant the decode is as of; fix it for reproducible output |
A VIN is never an error: what is wrong with it is in error_codes (vPIC's
codes, [0] is clean), with corrected_vin and the possible values when
the procedure suggests them. Every decode carries its data_month and
decoder_version.
CLI
nitrovin decode 1HGCM82633A004352 # JSON, pretty on a terminal
nitrovin decode -o text --model-year 2003 1HGCM82633A004352 # two columns
nitrovin decode --shape nitrovin --full 1HGCM82633A004352 # by variable name, with items
printf '1HGCM82633A004352,2003\n5YJ3E1EA2JF000316\n' | nitrovin decode - # one JSON line per VIN
nitrovin info # version, data month, artifact hash, elements
decode - reads one VIN per line, optionally followed by a comma or a tab
and its model year. Exit status is 0 when every VIN was decoded, whatever
its error codes; 1 when nitrovin could not answer (data that will not load,
a refused input line); 2 for a mistake on the command line, such as an
argument VIN over 17 characters. Problems go to stderr as one JSON line.
CLI contract.
HTTP
nitrovin serve # 127.0.0.1:8080; --addr, or $PORT, to change
curl localhost:8080/v1/decode/1HGCM82633A004352
curl 'localhost:8080/v1/decode/1HGCM82633A004352?model_year=2003&shape=nitrovin'
curl localhost:8080/v1/decode -d '[{"vin":"1HGCM82633A004352"},{"vin":"5YJ3E1EA2JF000316","shape":"nitrovin"}]'
curl localhost:8080/v1/info
curl 'localhost:8080/v1/translate?from.vin=1HGCM82633A004352'
curl localhost:8080/v1/vocabularies
curl 'localhost:8080/v1/vocabularies/vpic/entries?where.make=474&limit=5'
Open the address in a browser for a decode form: a VIN, an optional model
year, the shape and full, rendered on the server, each decode a
shareable URL (/?vin=1HGCM82633A004352). A decode shows its elements in
vPIC's groups (empty values hidden in the vpic shape until asked), its
error codes in NHTSA's words, a suggested VIN with the differing positions
marked, a link to the same VIN on NHTSA's decoder, and the decode's JSON,
URL, curl and nitrovin decode equivalents to copy. The empty form says
how to install, configure MCP and verify this version, with links to its
release and data month. The only script, /copy.js, adds copy buttons;
without JavaScript everything else works and the text stays selectable.
The form is not part of the API: it is not in the OpenAPI document and may
change.
POST /v1/decode takes up to 1,000 requests and returns an array of
decodes in order; POST /v1/translate does the same for translate
requests, and GET /v1/translate takes one as dotted query parameters
(from.vocabulary=yard&from.entry.make=Dodge&from.entry.model=Ram%201500).
GET /v1/vocabularies lists what is loaded and
GET /v1/vocabularies/{name}/entries pages through one
(Translation,
Listing). Refusals are
application/problem+json (RFC 9457) with a pointer to the offending
value. GET /v1/openapi.json serves the
OpenAPI 3.1 document; GET /healthz is the probe.
There are no CORS headers unless --cors <origin> (or --cors '*') names
the browsers that may call /v1 and /mcp. SIGINT or SIGTERM drains
requests in flight and exits 0.
stdio (JSON lines)
printf '{"id":1,"vin":"1HGCM82633A004352"}\n{"id":2,"vin":"5YJ3E1EA2JF000316","shape":"nitrovin"}\nnot json\n' | nitrovin stdio
One request per line in, one line out per request, in order and flushed per
line: {"id": …, "decode": {…}} or {"id": …, "error": {…}}. A line with
"op": "translate" or "op": "list" translates or lists instead, through
the --vocab files, and its reply is keyed translation or list. A bad
line gets an error line and the session goes on; EOF ends it. This is the
easiest way to keep one decoder process open from another language
(docker run --rm -i ghcr.io/joshuasprow/nitrovin stdio works too).
stdio contract.
MCP
nitrovin mcp is an MCP server over stdio with the tools decode_vin,
whose arguments are the request above, and translate and list, which
answer from the vocabularies --vocab loads
(tool definitions). For Claude Desktop
(~/Library/Application Support/Claude/claude_desktop_config.json on
macOS, %APPDATA%\Claude\claude_desktop_config.json on Windows) and Cursor
(~/.cursor/mcp.json) the entry is the same:
{
"mcpServers": {
"nitrovin": {
"command": "/opt/homebrew/bin/nitrovin",
"args": ["mcp"]
}
}
}
Use the absolute path which nitrovin prints: desktop apps do not see your
shell's PATH. With Docker instead, "command": "docker" and
"args": ["run", "--rm", "-i", "ghcr.io/joshuasprow/nitrovin", "mcp"].
A client that installs from the MCP Registry
finds this server as io.github.joshuasprow/nitrovin: the Docker image at
the release tag, run with mcp.
To try it without a client, save the snippet as claude_desktop_config.json
and call the tool with the MCP Inspector:
npx @modelcontextprotocol/inspector --cli --config claude_desktop_config.json --server nitrovin \
--method tools/call --tool-name decode_vin --tool-arg vin=1HGCM82633A004352
nitrovin serve serves the same server over Streamable HTTP at /mcp,
stateless and with JSON responses, for clients that take a URL instead of a
command ("url": "http://127.0.0.1:8080/mcp"):
npx @modelcontextprotocol/inspector --cli http://127.0.0.1:8080/mcp --transport http \
--method tools/call --tool-name decode_vin --tool-arg vin=1HGCM82633A004352
Or use the hosted one, https://nitrovin.app/mcp, with no install.
Go module
go get github.com/joshuasprow/nitrovin/pkg/nitrovin@latest
package main
import (
"encoding/json"
"fmt"
"log"
"github.com/joshuasprow/nitrovin/pkg/contract"
"github.com/joshuasprow/nitrovin/pkg/nitrovin"
)
func main() {
d, err := nitrovin.Embedded() // or nitrovin.Open("nitrovin-2026_10.artifact")
if err != nil {
log.Fatal(err)
}
req, err := contract.ParseRequest([]byte(`{"vin": "1HGCM82633A004352"}`))
if err != nil {
log.Fatal(err)
}
dec, err := d.Decode(req) // a VIN is never an error: see dec.ErrorCodes
if err != nil {
log.Fatal(err)
}
out, err := json.Marshal(dec)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(out))
}
A Decoder is safe for concurrent use. nitrovin.Embedded links the data,
about 61 MB, into your binary, and the module download includes it (a
20 MB zip of github.com/joshuasprow/nitrovin-data); nitrovin.Open loads
an artifact file instead, and a binary that never calls Embedded does not
carry the data. Every pkg/ package is public API:
pkg.go.dev.
C library
libnitrovin (.so, .dylib, .dll) and nitrovin.h carry no data:
nitrovin_open loads an artifact, the same file --data takes, from a
nitrovin-data release.
nitrovin_decode takes one stdio request line and returns exactly the
reply line nitrovin stdio would write; nitrovin_open_vocab opens the
artifact with vocabularies beside it, for translate and list lines
(the C library).
#include <stdio.h>
#include "nitrovin.h"
int main(void) {
char *err = NULL;
nitrovin_t *h = nitrovin_open("nitrovin-2026_10.artifact", &err);
if (h == NULL) {
fprintf(stderr, "%s\n", err);
nitrovin_free(err);
return 1;
}
char *reply = nitrovin_decode(h, "{\"id\":1,\"vin\":\"1HGCM82633A004352\"}");
puts(reply); /* {"id":1,"decode":{...}}, as `nitrovin stdio` writes it */
nitrovin_free(reply);
nitrovin_close(h);
return 0;
}
cc -o decode decode.c -L. -lnitrovin -Wl,-rpath,. && ./decode
Free every returned string with nitrovin_free, never free(), and never
unload the library. docs/c-library.md has the
downloads, Python through ctypes, and the rules;
the C ABI contract is
authoritative.
Verify a release
Releases are signed with cosign
keyless signatures from GitHub Actions: no key to fetch, the certificate
names the workflow that built the release. Every asset has a
<asset>.sigstore.json bundle and a line in checksums.txt.
nitrovin, from the directory you downloaded the assets to:
TAG=v0.1.2
cosign verify-blob --bundle checksums.txt.sigstore.json \
--certificate-identity https://github.com/joshuasprow/nitrovin/.github/workflows/release.yml@refs/tags/$TAG \
--certificate-oidc-issuer https://token.actions.githubusercontent.com checksums.txt
sha256sum --check --ignore-missing checksums.txt
cosign verify ghcr.io/joshuasprow/nitrovin:$TAG \
--certificate-identity https://github.com/joshuasprow/nitrovin/.github/workflows/release.yml@refs/tags/$TAG \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
nitrovin-data, the data releases (signed from main, not from a tag):
cosign verify-blob --bundle checksums.txt.sigstore.json \
--certificate-identity https://github.com/joshuasprow/nitrovin-data/.github/workflows/release.yml@refs/heads/main \
--certificate-oidc-issuer https://token.actions.githubusercontent.com checksums.txt
sha256sum --check checksums.txt
cosign verify-blob --bundle <asset>.sigstore.json … <asset> checks one
asset directly. A signature from any other workflow or tag is rejected.
The data
The data is NHTSA's vPIC (Vehicle Product
Information Catalog). NHTSA publishes it once a month as a Postgres dump on
its downloads page, and the
decoding procedure this project ports ships inside that dump. NHTSA's
terms of use say the
information "is considered public information and may be distributed or
copied", and NHTSA disclaims any warranty on it; as a work of the U.S.
Government it has no copyright (17 U.S.C. § 105). The reading behind this is
the dump research.
See NOTICE.
A data month (YYYY_MM) names one dump. nitrovin converts a dump into
an artifact, a 61 MB indexed file that is the same bytes whenever the
same dump is converted, and
nitrovin-data
releases it as v<format>.<YYYYMM>.<rebuild> (v1.202610.1). Each data
release keeps, forever: the artifact, NHTSA's dump zip (so a month can be
re-checked after NHTSA stops hosting it), manifest.json (the dump's URL,
size and SHA-256, the artifact's blake3, row counts, the importer commit),
the month's frozen parity key, its stale-cache list and the EPA vocabulary
(Vocabularies).
$ nitrovin info | jq '{decoder_version, data_month, data_source, artifact_hash}'
{
"decoder_version": "v0.1.2",
"data_month": "2026_10",
"data_source": "embedded",
"artifact_hash": "blake3:7518c5b46c4264fe94a8e5158cb1949b000e50803b36d388fec9ebe914a780cb"
}
To decode against another month, download its artifact and pass it with
--data (any command) or NITROVIN_DATA; --data wins, then
NITROVIN_DATA, then the embedded data:
nitrovin --data nitrovin-2026_10.artifact info
NITROVIN_DATA=nitrovin-2026_10.artifact nitrovin serve
An artifact loads when its format major matches the binary's (format 1
today); a mismatch is an error naming both. When NHTSA publishes a month,
an unattended refresh builds it, gates it and, if it passes, cuts a patch
release of nitrovin with it through every channel, so updating nitrovin is
how you get new data. A month that fails ships nothing and the previous
month stays current. There is no nitrovin update.
A decode is a function of the VIN, the model-year hint, now, the data
month and the decoder version, and it carries all five; the same five give
the same bytes. Live vPIC runs on newer data than any monthly dump, so it
can answer differently from nitrovin on the same day.
Vocabularies
Your own names for vehicles and part types (a yard's inventory, a parts
catalog's codes) load beside the data as vocabulary files, and nitrovin
translates a VIN or a name into every loaded vocabulary's names. A file is
JSON Lines, a header and one line per entry
(vocabulary.schema.json), in one of
three kinds: a vocabulary maps its entries onto vPIC's makes and models
(by id, with optional model-year ranges and qualifiers such as Drive Type)
or onto a part-type spine; an extension set mints the vehicles vPIC
lacks; a spine declares the part types a part vocabulary maps onto.
Matching is exact after one fixed normalization (case and spaces): rules
and fuzzy matching belong to whoever builds the file.
docs/vocabularies.md is how to write one, check
it, and translate and list on every surface.
Name files or directories with --vocab (repeatable, any command) or
NITROVIN_VOCAB. A file with a fault stops nitrovin from starting; where a
file disagrees with the data month (an id NHTSA dropped, say) nitrovin
warns on stderr and in nitrovin info, and serves it anyway. nitrovin vocab check fails on both, for your CI. From a checkout, with the
synthetic vocabularies in testdata/vocab:
nitrovin vocab check testdata/vocab
nitrovin --vocab testdata/vocab list # what is loaded
nitrovin --vocab testdata/vocab list yard --where make=dodge # a vocabulary's entries, paged
nitrovin --vocab testdata/vocab translate --to yard 1C6RR7LT5FS123456 # a VIN to your names
nitrovin --vocab testdata/vocab serve # /v1/translate, /v1/vocabularies
Each data release also carries epa-vehicles.jsonl, a vocabulary of the
names EPA's fuel-economy data
(vehicles.csv) gives makes
and models, mapped onto that month's vPIC makes and models with Drive Type
and Series qualifiers where the name implies them. The monthly refresh builds
it from that day's vehicles.csv against the new month and publishes it only
if it loads with no fault; its version is EPA's newest record date and the
data month (2026-11-03+2026_10), and the refresh PR reports its drift.
Download it from the release of the month your binary embeds (nitrovin --version names it), or the latest:
curl -fsSLO https://github.com/joshuasprow/nitrovin-data/releases/latest/download/epa-vehicles.jsonl
nitrovin --vocab epa-vehicles.jsonl info | jq '.vocabularies[] | {name, version, drift}'
nitrovin --vocab epa-vehicles.jsonl translate --to epa-vehicles 1C6RR7LT5FS123456
A vocabulary built against another month loads too, with a drift warning for
each id NHTSA has changed since. checksums.txt on the same release covers
it (Verify a release). nitrovin ships no other
vocabulary, and nitrovin.app loads none.
Parity
nitrovin's answer for a VIN is NHTSA's procedure's answer, not an
approximation of it: every row the procedure emits, all 15 columns, exact
strings, compared as a multiset per VIN. There is no tolerance and no
per-VIN excuse list. The oracle is NHTSA's own spvindecode run in Postgres
17 on the month's dump, with one stale cache table emptied: the dump's
cache of valid characters contradicts the dump's own pattern rows, and live
vPIC answers the way the recomputation does. That is the one registered
deviation, stale-cache,
and the corpus VINs it touches are listed with each data release.
The 2026_10 data in v0.1.2 passed refresh run 42662006323: 18,339 calls and 966,232 rows, 0 disagreements; every one of the month's 13,014 WMIs and 25,283 of its 25,293 VinSchemas reached; 52 stale-cache calls; all 7 deviation witnesses agree with live vPIC on 253 fields. Its report is the body of the refresh PR.
docs/parity.md explains the corpus and the gates, where each month's report lives, and how to re-run a release's parity check yourself in a second.
Known limits
- 10 VinSchemas are never exercised. In 2026_10, schemas 4329, 5046, 6173, 11117, 11146, 11395, 11396, 13555, 15597 and 21187 lose to another schema on the same WMI and years on every VIN tried, so no gated VIN reaches them (run 42662006323, #72).
- Deviations are classes, never VINs. Where nitrovin disagrees with NHTSA's procedure, it is a bug unless a registered class proves a defect in NHTSA's data, with a mechanical detector and a VIN on which live vPIC agrees with nitrovin (ADR 0006, the registry). There is one class.
- The
vpiclayout is not liveDecodeVinValuesbyte for byte. It has noVINkey, hasPlantAddress, lacks vPIC's ten always-empty legacy keys, and joins multi-valued elements in byte order where vPIC's order is unstable (details). Parity is gated on the items both layouts are built from. - Element names and codes are data, not API. NHTSA renames them between
months; pin
element_idandattribute_idfor anything long-lived. - Over 17 characters is refused. NHTSA's procedure raises on it, so there is no answer to match.
- Not drop-in for vPIC's URLs.
servehas its own/v1routes, not vPIC's/api/vehicles/.... - winget is not live yet (#78). macOS binaries are not notarized (see Install).
- The C library cannot be unloaded, needs glibc 2.17+ on Linux (no musl), and macOS 11+.
Performance
On one core of an Apple M5 Pro, v0.1.2 decodes about 17,600 VINs a second
through nitrovin decode - with JSON output (25,000 corpus VINs in 1.42 s
under GOMAXPROCS=1), and a one-VIN nitrovin decode process takes about
6 ms and 22 MB of memory.
Contributing
Issues and pull requests are welcome. CONTRIBUTING.md
covers building, the tests, the Postgres oracle on your own machine, and the
rules for a change that touches decoding. Decisions live in
docs/adr/, words in GLOSSARY.md, and the plan
in the wayfinder map.
Credits and licence
MIT, see LICENSE. The data is NHTSA's. The port's structure,
its parity method and its deviation registry follow
ultravin by Bryce
Drennan (MIT), the reference implementation; no ultravin source is copied,
and the committed parity corpus includes ultravin's VIN lists. Details in
NOTICE.
Comments
New to MCP?
From what the protocol is to installing your first server, these answers cover most first questions.
An open protocol that defines how AI applications connect to external tools and data in a uniform way. Clients talk to the model; MCP servers provide tools, resources and prompt templates. They exchange standard messages, so one server can be reused by many clients.