Case Study · Chrome Extension
How I Built a Chrome Extension to Automate US Visa Appointment Slot Checking
By Mohammad Tanvir·February 3, 2026·12 min read
If you have ever tried to book a US visa interview appointment from Bangladesh, you already know the reality: slots open without warning and vanish within seconds. Thousands of applicants are competing for the same handful of dates on usvisascheduling.com, and manually refreshing the page is a losing strategy.
So I built a Chrome extension that does it automatically. It checks for available slots on a precise, configurable loop, parses the appointment calendar’s DOM to confirm real availability, and fires a Gmail notification the instant a slot opens. This post is a full technical breakdown of how it works — from Manifest V3 architecture to persistent timing across page reloads to two-layer slot detection logic.
📌
What you will learn
How to structure a Manifest V3 Chrome extension for web automation, how to persist state across page navigations using chrome.storage.local, how to parse a jQuery UI datepicker calendar to detect available dates, and how to automate Gmail compose without any email API.
Why This Problem Needed a Bot
The US visa scheduling portal is shared by applicants across the entire country. A single appointment slot can go from available to booked in under three seconds. Manual checking — even if you refresh every minute — is almost guaranteed to miss openings, especially during off-peak hours when new slots are quietly added.
Existing tools in the market either require paid subscriptions, depend on crowdsourced community data, or only work for specific visa types and regions. I needed something that worked directly on usvisascheduling.com for the DHAKA consular post, ran silently in the background, and notified me the moment something changed.
⚠️
Important note
The portal can lock you out for 72 hours if it detects bot-like behavior from aggressive refreshing. The extension uses configurable, human-like intervals (30–60 seconds is recommended) to avoid triggering these restrictions.
Extension Architecture at a Glance
The extension is built on Chrome Manifest V3, which is the current standard for browser extensions. It uses the three-context architecture that Manifest V3 requires: a popup for the user interface, a service worker for background tasks, and content scripts that run directly on the target pages.
US-Visa-Scheduler-Bot/
├──manifest.json← extension config, permissions, script bindings
├──popup.html← user interface (email, intervals, start/stop)
├──popup.js← validation, storage, sends messages to content script
├──background.js← service worker, desktop notifications
├──content.js← main bot: cycle loop, detection, cooldown
└──gmail-composer.js← injects email into Gmail compose window
content.js is the heart of the extension — it runs on usvisascheduling.com and handles the entire automation loop. gmail-composer.js is a second content script that runs on mail.google.com and auto-fills the compose window when the bot decides to send a notification.
How the Automation Cycle Works
The bot operates by navigating between two pages on the visa site in a repeating loop. Each full cycle looks like this:

Selecting DHAKA is not just setting a dropdown value. The bot programmatically sets the value property of the <select> element and then dispatches a native change event. This is important because the site uses JavaScript event listeners to load the calendar — simply changing the value without firing the event would result in no calendar appearing.
The Hardest Problem: Timing Across Page Reloads
Here is the tricky part that most people overlook when building automation extensions: every single cycle destroys the content script. After checking the schedule page, the bot navigates back to the home page. That navigation reloads the page, which kills all JavaScript variables, all setInterval timers, everything.
A naive implementation would store the “next cycle” timestamp in a JavaScript variable. It would reset to zero on every reload, making the bot fire immediately every time instead of waiting the configured 50 seconds. This was the exact bug I debugged early in development.
✅
The fix: chrome.storage.local
All timing state is persisted in Chrome’s local storage, which survives page loads. The bot stores absolute timestamps — not durations — so after a reload it simply reads the target time from storage and waits only the remaining seconds.
Here is the core timing logic:
content.js — Timing Loop
// Runs every 1 second — checks if it is time to fire
setInterval(async () => {
chrome.storage.local.get(['nextCycleTime'], (result) => {
const now = Date.now();
if (now >= result.nextCycleTime) {
// Save the NEXT target before the page navigates away
chrome.storage.local.set({
nextCycleTime: now + botConfig.interval,
lastCycleTime: now
});
runBotCycle(); // ← this will navigate the page
}
});
}, 1000); // poll every second for precision
The key insight: nextCycleTime is an absolute Unix timestamp, not a relative duration. After the page reloads and the script restarts, it reads that same timestamp from storage. If 30 seconds remain, it waits 30 seconds. If the time has already passed (because the reload took a while), it fires immediately. No drift, no instant-fire bugs.
Two-Layer Slot Detection Logic
This is where the project gets interesting. The visa scheduling site has an inconsistency: sometimes it displays a clear “No Slots Available” banner message. But other times that banner disappears entirely, and instead the site renders a full two-month calendar where every single date is marked as unavailable. If you only check for the banner text, you will get false positives — the bot would think slots are available when they are not.
The solution is a two-step detection pipeline.
Step 1 — Check the Page Text
The first check is simple and fast. The bot scans the entire page body for the exact string "No Slots Available". If found, the decision is immediate: no slots, move on.
content.js — Step 1
const bodyText = document.body.innerText;
const noSlotsText = bodyText.includes('No Slots Available');
if (noSlotsText) {
// Definite answer — no need to check the calendar
sendEmailWithCooldown(false);
return;
}
// Text not found — proceed to Step 2
Step 2 — Parse the Calendar DOM
If the banner text is missing, the bot inspects the jQuery UI Datepicker calendar that the site renders. Every date on the calendar is a <td> element. Unavailable dates carry specific markers that the bot checks for:
- The CSS class
redday— visually marks the date red - The CSS class
ui-state-disabled— makes the date unclickable - The attribute
title="No Available Appointments"— the tooltip text
A date is only considered available if it has none of these markers.
content.js — Calendar Inspection
const datepicker = document.querySelector('#datepicker');
if (!datepicker) {
// Calendar not found → safe default: no slots
return { hasAvailableSlots: false };
}
// Get all date cells, excluding other-month padding
const allCells = datepicker.querySelectorAll(
'td:not(.ui-datepicker-other-month)'
);
// Filter to only truly available dates
const availableDates = Array.from(allCells).filter(cell => {
return !cell.classList.contains('redday')
&& !cell.classList.contains('ui-state-disabled')
&& cell.getAttribute('title') !== 'No Available Appointments';
});
return { hasAvailableSlots: availableDates.length > 0 };
Here is how all four possible scenarios map to the final decision:
❌ Scenario A
“No Slots Available” banner text is found on the page. Decision made at Step 1. Calendar is never checked.
❌ Scenario B
Banner text is missing. Calendar element is also not found on the page (e.g. during a page transition). Defaults to no slots — the safest choice.
❌ Scenario C
Banner text is missing. Calendar is present, but every single date cell has the redday class. Zero available dates found.
✅ Scenario D
Banner text is missing. Calendar is present, and at least one date cell has no unavailability markers. Slots are available.
The “safe default” philosophy behind Scenario B is deliberate. A false positive — alerting you that slots exist when they do not — wastes your time and erodes trust in the tool. A false negative — missing slots for one 50-second cycle — is far less costly, because the bot will catch them on the very next check.
Smart Email Notifications With Cooldown
Sending an email every single cycle would flood your inbox. But you also can not afford to miss the one moment when a slot actually opens. The notification system solves this with two independent cooldown rules:
| Condition | Email Behavior |
|---|---|
| Slot is available | Immediate — zero cooldown. Fires the instant a slot is detected. |
| No slots (first check) | Immediate — no previous timestamp to compare against. |
| No slots (repeating) | Throttled — only sends after the configured notification interval (e.g. 200s) has elapsed. |
Like the cycle timing, these cooldown timestamps are stored in chrome.storage.local so they persist across reloads. Here is what a typical session looks like with a 50-second loop and a 200-second email interval:
0:00 → Cycle 1 → No slots → ✉️ Email sent (first notification)
0:50 → Cycle 2 → No slots → ⏳ Cooldown — 150s remaining
1:40 → Cycle 3 → No slots → ⏳ Cooldown — 100s remaining
2:30 → Cycle 4 → No slots → ⏳ Cooldown — 50s remaining
3:20 → Cycle 5 → No slots → ✉️ Email sent (200s elapsed)
4:10 → Cycle 6 → ⚡ SLOT FOUND → ✉️ Email sent immediately. Bot stops.
Automating Gmail Without Any API
One of the more creative parts of this extension is how it sends email. It does not use Gmail’s API, SMTP, or any third-party email service. Instead, it opens a Gmail compose tab and fills it in programmatically using a content script.
The flow works like this: when the bot decides to send a notification, it saves the email payload (recipients, subject, body) to chrome.storage.local, then opens a new tab pointed at Gmail’s compose URL. The gmail-composer.js content script, which is registered to run on mail.google.com, detects when the compose window is ready, reads the payload from storage, fills in the To, Subject, and Body fields, and clicks Send.
This approach means the extension never touches your email credentials and works entirely within the browser you are already logged into.
Why Manifest V3 and What It Means
Chrome has moved all extensions to Manifest V3, which replaces the old background pages with service workers. Service workers can be terminated by the browser when idle, which means you cannot rely on a background script to keep running indefinitely. This extension is designed around that constraint:
- All persistent state lives in
chrome.storage.local, not in memory. - The content script re-reads state from storage on every page load and auto-resumes if the bot was previously running.
- The service worker (
background.js) only handles one-shot tasks like creating desktop notifications — it does not need to stay alive.
How This Compares to Existing Tools
There are a handful of other visa slot monitoring tools on the Chrome Web Store. Here is how this extension stacks up on the features that actually matter:
| Feature | Community Tools | Paid Services | This Extension |
|---|---|---|---|
| Works directly on usvisascheduling.com | Yes | Yes | Yes |
| Zero cost | Yes | No | Yes |
| Configurable check interval | No | Yes | Yes |
| Separate email cooldown | No | No | Yes |
| Calendar DOM verification | No | No | Yes |
| No account or login required | No | No | Yes |
| Multi-recipient email | No | Yes | Yes |
Technical Skills This Project Covers
Building this extension touched on a wide range of real-world developer skills. If you are working through a similar project or preparing for a portfolio, here are the key areas:
Final Thoughts
This project started as a simple automation script and turned into a genuinely interesting engineering challenge. The three problems that made it non-trivial were all related to the nature of browser extensions: state that disappears on page navigation, timing that must survive reloads, and detection logic that has to account for inconsistencies in the target website’s HTML.
If you are interested in building similar automation tools — whether for visa appointments, job board monitoring, e-commerce price tracking, or anything else — the patterns used here (persistent timestamps in extension storage, content script message passing, safe-default fallback logic) apply directly. The full source code is on GitHub.
Want to see the code?
The full extension source is open and ready to clone. Check out the GitHub repo or explore the rest of the portfolio.
© 2026 Mohammad Tanvir — Python Web Scraping & AI Automation Specialist · GitHub · LinkedIn
