How I Built a Chrome Extension to Automate US Visa Appointment Slot Checking | Mohammad Tanvir

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:

ConditionEmail Behavior
Slot is availableImmediate — 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:

FeatureCommunity ToolsPaid ServicesThis Extension
Works directly on usvisascheduling.comYesYesYes
Zero costYesNoYes
Configurable check intervalNoYesYes
Separate email cooldownNoNoYes
Calendar DOM verificationNoNoYes
No account or login requiredNoNoYes
Multi-recipient emailNoYesYes

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:

Chrome Manifest V3 Service Workers Content Scripts chrome.storage.local DOM Parsing Event Dispatching Async/Await jQuery UI Datepicker Gmail Automation Web Scraping Browser Notifications API State Persistence Concurrency Control JavaScript ES2017+

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

Leave a Reply