ca-dmv-bot: a social bot that publishes California vanity plate records
Social media bot that posts personalized license plate applications the California DMV received
At a glance
- What is it?
- ca-dmv-bot takes 35,509 personalized license plate applications the California DMV received between 2015 and 2017 and posts them as rendered images to Tumblr and Bluesky, with a Discord queue for manual approval before anything goes out. It is a public records project, and the author describes it as made on a slow weekend with almost no error checking.
- Who is it for?
- Adopt ca-dmv-bot if you want a working example of a multi-network poster built on public records data, or you want to run the same vanity plate feed for a community that has asked for it, and you are prepared to run the moderation queue yourself. Do not adopt it as a general purpose social posting bot, because the approval workflow, the image generation and the data set are all specific to this one project.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 5 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A bot that turns a public records request into social posts
ca-dmv-bot is a social media bot that posts personalized license plate applications the California DMV received. The description in the package manifest is more precise: it posts public records of California DMV vanity license plate submissions, and the project is a JavaScript application with app.js as its entry point.
The data comes from a separate project, 21five/ca-license-plates, and the README quotes its description. It holds 35,509 personalized license plate applications that the California DMV received from 2015 through 2017. It is not all the applications the DMV reviewed in that period, only those flagged for additional review by the Review Committee. And it was compiled from 623 Excel workbooks that the DMV prepared for a public records request.
That last detail is the one that makes the project possible, and it also explains its boundaries. A public records request produces spreadsheets, and this repository turns a set of spreadsheets into a stream of images, one per application, and posts them.
The bot is live on Tumblr as @ca-dmv-bot and on Bluesky as @ca-dmv-bot.bsky.social. The repository is MIT licensed, the default branch is trunk, and the last push was on 2026-10-19 with no GitHub releases published.
Discord sits in the approval path, not beside it
The Notes section lists what the bot does and one entry is more important than the rest. It uses Discord to manually approve plates, to ensure no obscene content gets posted, such as slurs.
That is a design decision with real consequences, and it is the right one given the data. A vanity plate dataset contains whatever people put on their plates, and some of it is a slur aimed at somebody. Automating the posting of that would mean publishing abuse to followers of a bot, so a human in the loop sits between the data and the post.
The mechanics of that are visible in the environment configuration. The .env.example lists a Discord token, a Discord channel id, a Discord moderator role id and a Discord owner user id, alongside credentials for Twitter, Mastodon, Tumblr and Bluesky. The channel, the role and the owner id are three different levels of authorisation, which suggests a moderation queue that distinguishes who may approve, who may administer and who owns the deployment.
The environment file also shows the network configuration, with a Mastodon URL and access token, four Tumblr credentials, four Twitter credentials, and a Bluesky service URL with an identifier and password. The Bluesky service defaults to https://bsky.social, and the bot account is separate from the personal identifier used to log in.
GraphicsMagick renders the plate image, and a custom font draws it
The image generation is a separate concern from the posting, and it is handled by GraphicsMagick, described in the README as a fork of ImageMagick. The dependency list confirms it with the gm package at version 1.25.0, and the Dockerfile installs it in the same line that sets up the runtime image.
That Dockerfile is short enough to read as the whole deployment story:
FROM node:lts-alpine
RUN apk add --no-cache graphicsmagick
WORKDIR /app
COPY . .
RUN npm install
RUN npm install -g pm2 babel-cli
CMD ["pm2-runtime", "start", "pm2.json"]The base image is the Node LTS Alpine variant, GraphicsMagick is added with apk, the application is copied in, dependencies are installed, and pm2 and babel-cli are installed globally. The container then starts through pm2-runtime with the pm2.json file, which is the process manager configuration rather than a plain node invocation. The package script for running it directly is node --no-warnings app.js, so pm2 is the supervised path and that script is the manual one.
The typography is a custom font, credited to Ethel, who also made the profile picture and the banner. The font is Cascadia Code, which the README notes is licensed under the SIL Open Font License, so the bot's own MIT licence does not extend to it.
The output format is a rendered image rather than a text post, which is the reason a graphics library is a dependency at all. A plain text post of a plate number would need nothing.
The post is not a screenshot, it is a generated card
The dependency set tells you how much of the work is done by hand. There is csv-parser, so the source data is in CSV form and is parsed rather than hand-transcribed. There is string-to-stream, so files are handed to a process as streams. There is node-schedule, which is what makes the posting interval a configuration value rather than a loop in the code. And there is fs-extra, which is the usual choice when a script needs to move and copy files around while generating images.
The content of the post is deliberately limited, and the README says why: it does not include review reason codes for brevity purposes. The plates that went to the Review Committee are in the source data, and the reason each one was flagged is also in the source data, and the bot leaves that out. The result is a post about the plate, not about the review.
The README also states the data range, 2015 to 2017, which means the feed is a closed historical set rather than a live feed. A bot that publishes a fixed set of records will eventually run out of new ones, which is a different kind of project from one that scrapes a live source.
The authors thanked in the README are Noah Veltman and 21five for compiling the data, Ethel for the font and the artwork, and the author's friends for helping moderate the bot. That last credit is a good indicator of the effort involved, since moderation is a human job that runs for as long as the bot is up.
Ephemeral logging and almost no error checking
There is a line in the Notes that deserves to be quoted rather than paraphrased: made on a slow weekend, only ephemeral logging exists and there is little-to-no error checking.
That is an accurate self-assessment and it defines how you should treat the project. Ephemeral logging means there is no durable record of what was posted, when, or whether a post succeeded, so if something goes wrong you are reconstructing from the platform's own state rather than from the bot's. Little-to-no error checking means a failure in the pipeline, a missing image, a network error from one of four services, can pass silently.
For four social networks, each with its own client library, that is a real risk. The dependencies are twitter-api-v2 for Twitter, masto for Mastodon, tumblr.js for Tumblr and @atproto/api for Bluesky. Each has its own authentication model and its own idea of what a post is, and the code has to keep the plate text, the image and the approval state in step across all of them. The Discord approval is the synchronisation point, and it is the part that is most likely to hold up under a slow or flaky network.
None of this makes the project unsuitable for what it is. It makes it a good example of a working personal project and a poor foundation for something you need to be reliable. The repository also has a Dockerfile, a pm2.json, a .gitmodules, a networks/ directory and a resources/ directory, so the deployment shape is there even if the internals are informal.
A different approach from scraping a live feed
The alternative to this project is a scraper that watches a live source and posts from it as data arrives, which is the standard shape for a social bot and the one most public records projects end up with.
The difference in approach is the whole point here. A scraper needs to handle a site changing its layout, rate limits, and pagination, and it has a continuity problem: when it breaks, the feed goes quiet and nobody notices until a reader complains. ca-dmv-bot has none of that, because there is nothing to scrape. The data is a static set of 623 workbooks compiled once, and the bot's job is only to work through it.
The trade is that it cannot report anything new. The 2015 to 2017 window is fixed, so the value of the bot is in resurfacing records people have not seen, not in delivering current information. For a public records project that is a reasonable design, because the records are the interesting artefact and the distribution mechanism is the boring part.
The other difference is the moderation design, and it is not a small one. A scraper usually filters what it posts with a list of banned words. This bot sends each plate to a Discord channel and waits for a person, which costs human attention per post and scales badly to thousands of posts, but it catches the things a word list cannot anticipate, such as a plate that is not itself a slur but is a specific person's name.
A third option worth naming is a static archive. A blog post per plate, or a single browsable index, would give readers the same records with none of the social posting machinery, and it would not need image generation, four API clients or an approval queue. The bot exists because the pacing and the serendipity of a feed are worth something to someone.
Editorial conclusion
Adopt ca-dmv-bot if you want a working example of a multi-network poster built on public records data, or you want to run the same vanity plate feed for a community that has asked for it, and you are prepared to run the moderation queue yourself. Do not adopt it as a general purpose social posting bot, because the approval workflow, the image generation and the data set are all specific to this one project. Before you deploy it, read the .env.example and fill in credentials for the networks you actually want, and note that the project is a working weekend build with little error checking and only ephemeral logging, so you should read the pm2.json and the .env.example before you point it at real accounts.
Frequently asked questions
What data does ca-dmv-bot publish?
It posts personalized license plate applications the California DMV received from 2015 through 2017, sourced from the 21five/ca-license-plates project. That dataset holds 35,509 applications compiled from 623 Excel workbooks prepared for a public records request, and covers only applications flagged for additional review by the Review Committee.
How does ca-dmv-bot decide what to post?
It uses Discord to manually approve plates, so that obscene content such as slurs is not published. The environment configuration includes a Discord token, channel id, moderator role id and owner user id for that workflow, and the README says review reason codes are left out for brevity.
Which social networks does ca-dmv-bot post to?
The bot is live on Tumblr as @ca-dmv-bot and on Bluesky as @ca-dmv-bot.bsky.social, and the package dependencies include clients for Twitter, Mastodon, Tumblr and Bluesky. The .env.example file holds the credentials for each, with the Bluesky service defaulting to https://bsky.social.
How do I run ca-dmv-bot?
The package script is `node --no-warnings app.js`, and the Dockerfile starts it with pm2-runtime using pm2.json after installing dependencies and graphicsmagick on the node LTS Alpine image. Configuration is read from environment variables, with .env.example listing every variable the bot expects.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/rjindael-ca-dmv-bot)Was this useful?