A daily check that warns me before a domain expires

Christian Drexler

At the start of October I went through my registrar accounts for an unrelated reason and found three things in ten minutes. One domain had expired two days earlier. Two more, including the one this site runs on, were twelve days from expiry, set to auto-renew, on an account holding twelve cents. And one domain that carried a live page had auto-renew switched off and seven weeks left.

Nothing had broken yet. That was luck. Nobody was watching the dates, because everybody, me included, assumed auto-renew was.

Why auto-renew is not a safety net

Auto-renew is a promise the registrar keeps only if it can charge something. A card that expired, a card that was replaced after a fraud alert, an account balance that ran down: in all three cases the renewal fails and the first notice lands in a mailbox nobody reads, because registrar mail looks like marketing.

The registrar's API can tell me the expiry date and the account balance. It cannot tell me whether the card on file still works. So the only thing I can watch reliably is the date itself. If a domain that should have renewed weeks ago is suddenly fourteen days from expiry, the renewal has failed, whatever the settings say.

The four buckets

The job runs once a day in the morning. It pulls the full domain list from each registrar account, plus the balance, and sorts every domain into one of four buckets or into none.

  • Expired. The registrar says so, or the date is in the past.
  • Critical. Fourteen days or less, whatever auto-renew says. A card failure shows up here first.
  • Warning. Thirty days or less with auto-renew off. This one is a decision, not an accident: either renew it or let it go on purpose.
  • Note. The account balance is under fifteen dollars while something is due to renew within thirty days.

Everything else is fine and gets no line. A report with sixty healthy domains in it is a report nobody reads on day three.

Reading the dates without an API key

Most registrars have an API, and most of them make you whitelist the calling IP address first, which is a small project of its own if your machine sits behind a home connection. For a first version you need neither. Every domain's expiry date is public through RDAP, the successor of whois, and it comes back as JSON.

import json, urllib.request, datetime as dt

def days_left(domain):
    with urllib.request.urlopen(f"https://rdap.org/domain/{domain}", timeout=20) as r:
        data = json.load(r)
    for ev in data.get("events", []):
        if ev.get("eventAction") == "expiration":
            exp = dt.datetime.fromisoformat(ev["eventDate"].replace("Z", "+00:00"))
            return (exp - dt.datetime.now(dt.timezone.utc)).days
    return None

for d in ["example.com", "example.dev"]:
    print(d, days_left(d))

rdap.org redirects to the registry that is responsible for the ending, so the same line works for .com, .dev, .io and most others. A few country endings do not publish an expiry date; for those the function returns None, and None belongs in the report as its own line. A date I cannot read is not a healthy domain. It is an unknown one.

RDAP shows the registry's date, which can differ by a few days from the date in the registrar's panel, because some registrars renew at the registry early and bill later. For a fourteen-day threshold that does not matter.

The registrar API becomes worth the trouble once you want the two things RDAP cannot see: the auto-renew flag and the account balance. Those two fields are what separate "will renew by itself" from "will renew by itself if the money is there".

When it speaks

A check that posts the same warning every morning trains you to ignore it. So the job keeps a small state file with a fingerprint of the current findings, and the rules for posting are these:

  1. The set of findings changed since the last post: post.
  2. Something is expired or critical: post every day until it is fixed. This is the one case where repetition is the point.
  3. It is Monday: post a short digest, even when nothing changed, so I know the job is alive.
  4. Otherwise: stay quiet.

The Monday digest matters more than it looks. A watcher that is silent because everything is fine and a watcher that is silent because it crashed three weeks ago look exactly the same from the outside.

The fixed address problem

Once I moved from RDAP to the registrar's API, the whitelist came up. My registrar only answers calls from IP addresses entered in the account beforehand. The machine that runs my scheduled jobs sits on an ordinary connection whose address changes.

The way around it is a small server with a static address that I already had. Its address is on the whitelist. The daily job connects to it over SSH, runs the two API calls there and reads the answer back. The server stores nothing and decides nothing. It lends its address.

The API key lives in a file that only the job's user can read. A scheduled job should not depend on a password manager being unlocked at seven in the morning.

What it does not do

It does not renew anything. It reads and it reports. Renewing a domain is a payment, and I want a person to make payments, with the list in front of them, deciding at the same moment which domains are not worth another year. An automatic renewal keeps paying for all of them.

It also never touches DNS. A job that can read the registrar account is useful. A job that can change it is a risk I do not need for a date check.

What I would do first

If you own more than five domains, run the RDAP snippet over the list today. It takes two minutes and needs no credentials. Then put it on a schedule, add the fourteen-day line, and send the output somewhere you already look. The registrar API, the balance check and the buckets can come later.

The domain that had expired two days before I looked was one nobody needed. It could just as well have been the one with the checkout on it.