Diagnostic Domain Lifecycle

Auto-renewal enabled but payment failed

Auto-renew is on. You checked the box years ago and never thought about it again. But the expiration date at the registry keeps getting closer, because the card on file was declined and the renewal charge never actually went through. Auto-renew being on is a setting, not a guarantee. Here is why the charge fails quietly, how to catch it before the domain lapses, and a script that watches for it on a schedule.

RDAP expiry + status check Python and Node.js Safe by default (dry run)
A calculator and a laptop
Photo by Mehdi Mirzaie on Unsplash
The short answer

Auto-renew only works if the charge to the payment method on file actually succeeds. When the card is expired, declined, or flagged by the bank as suspicious, the renewal attempt fails, no charge posts, and the domain's real expiration date at the registry does not move, even though the auto-renew toggle in the registrar dashboard still shows on. Check the true expiration date with RDAP, the modern replacement for WHOIS, and treat a domain that has not moved forward while inside its warning window as a payment failure. The fix happens at the registrar: update the card, retry the charge, or pay the renewal manually. Full checks, a script, and tests are below.

The problem in plain words

Auto-renew is a promise the registrar makes to attempt a charge before the domain expires, not a promise that the charge will succeed. When you turned it on, the registrar stored a payment method and a rule that says "charge this card automatically a few weeks before the deadline." That rule fires on schedule every time. What it does not guarantee is that the card is still good when the rule fires.

Cards expire, get replaced after a bank reissues them, get declined for insufficient funds, or get blocked by the bank's fraud system because a charge from a domain registrar looks unfamiliar. When any of that happens, the renewal attempt fails, the registrar usually retries once or twice over the following days, and then it gives up. The auto-renew toggle in the account still says on, because nobody turned it off. The domain simply never got renewed, and the expiration date at the registry keeps counting down exactly as if auto-renew had never been enabled.

Auto-renew fires weeks before expiry Card declined or expired, retries fail no charge posts Toggle still says on, nothing renewed Expiry keeps counting down the toggle is a setting, not proof the charge went through
Auto-renew being on only means the registrar tried. The expiration date only moves when the charge actually succeeds.

Why it happens

This is a billing and account state at the registrar, not a DNS zone record, so it cannot be fixed by editing A, MX, or TXT records. A few common reasons the charge fails while auto-renew still shows as enabled:

The registrar dashboard almost always has a quiet billing or renewal history log that shows the failed attempt, but very few people check it unless something else prompts them to look.

The key insight

Auto-renew on means "a charge will be attempted." It does not mean "the domain is renewed." The only fact that actually matters is whether the expiration date at the registry moved forward. RDAP tells you that directly. If the warning window keeps closing in and the date has not moved, the payment failed, no matter what the toggle in the dashboard says.

The fix, as a flow

Do not trust the toggle. Query RDAP for the domain's real expiration date and status on a schedule, and treat a date that has not moved while inside the warning window as a failed charge. Then go to the registrar directly to update the payment method and retry or pay manually. On the DNS side, make sure a CAA record with an iodef contact is in place, so that if the domain ever does slip into a grace period, anything watching certificate issuance for it also has a channel to alert someone.

Query RDAP on a schedule Compare dates last seen vs. now Classify severity stalled + close to expiry? Charge looks failed? yes no, all clear Alert + fix CAA iodef via Cloudflare API
Detection and the CAA safety-net record can be handled automatically. Updating the card and paying the renewal itself always happens at the registrar.

How to fix it

1

Check the real expiration date with RDAP

RDAP is the modern, ICANN-mandated replacement for WHOIS. It returns a clean, structured expiration event. A bad result is an expiration date that has not moved forward from the last time you checked, even though auto-renew should have pushed it out by a year or more by now.

Terminal
check-rdap-expiry.sh
curl -s https://rdap.org/domain/example.com \
  | jq -r '.events[] | select(.eventAction=="expiration") | .eventDate'

# Example bad output if this is unchanged since last month's check:
# 2026-08-05T04:00:00Z
2

Check the status field for a grace period flag

If the charge failed and the domain already slipped past its expiration date, the status field will say so before anything else does. A bad result is autoRenewPeriod, redemptionPeriod, or pendingDelete.

Terminal
check-domain-status.sh
curl -s https://rdap.org/domain/example.com | jq -r '.status'

# Concerning statuses:
# "autoRenewPeriod"
# "redemptionPeriod"
# "pendingDelete"
3

Confirm the failed charge in the registrar's billing history

Log into the registrar account and look at the billing or renewal history for the domain. Nearly every registrar logs a declined or failed renewal attempt here, even when the alert email was never seen. This is the direct confirmation that the payment, not just the reminder, is the actual problem.

4

Update the payment method on file

There is no DNS record for this. Replace the expired or declined card, or reconnect the wallet, directly in the registrar account's billing settings, then trigger a manual retry of the renewal charge if the dashboard offers one instead of waiting for the next scheduled attempt.

5

Pay the renewal manually if the retry window already closed

If the registrar has stopped retrying, use the manual Renew now flow and pay for the extension directly, rather than waiting for auto-renew to try again on its own schedule, since some registrars only retry a fixed number of times.

6

Reconcile the CAA iodef alert record through the Cloudflare API

This is the one piece that is a real DNS zone record and is safe to automate. A CAA record with an iodef tag gives certificate authorities, and some monitoring tools, a mailbox to report problems to. Keeping it pointed at an address someone actually reads is a cheap safety net that a script can maintain on its own, separate from the registrar-side payment fix.

DNS record
zone record
; alert channel for anything watching this domain's certificates and status
example.com. 3600 IN CAA 0 iodef "mailto:domains@example.com"

; keep existing issue records unchanged alongside it
; example.com. 3600 IN CAA 0 issue "letsencrypt.org"

How to check it worked

Re-run the RDAP check after the registrar confirms the renewal went through, and confirm the CAA iodef record resolves to the address you expect.

Terminal
verify.sh
curl -s https://rdap.org/domain/example.com \
  | jq -r '.events[] | select(.eventAction=="expiration") | .eventDate'

curl -s https://rdap.org/domain/example.com | jq -r '.status'

dig +short CAA example.com

dig +short example.com
dig +short MX example.com

A good result looks like this: the expiration event date has jumped forward by roughly a year or more, for example from 2026-08-05 to 2027-08-05, and the status array no longer shows autoRenewPeriod, redemptionPeriod, or pendingDelete. The CAA lookup includes the iodef record pointing at a mailbox someone monitors. The A and MX lookups still return the expected records the whole time, since none of this ever touched the zone data itself.

The full code

This script detects a stalled renewal by comparing the RDAP expiration date against a previously recorded value and the current status, using a pure, I/O-free decision function. When it finds a real problem, it reconciles the one thing that is actually a DNS zone record, the CAA iodef alert contact, through the Cloudflare API. It never touches billing or the registrar account, since that is not something a DNS provider's API can reach.

View this code on GitHub Full runnable folder with tests in the dns-fixes repo.

auto_renewal_payment_failure.py
"""Detect a stalled auto-renewal caused by a failed payment charge, using
RDAP as the source of truth, and reconcile the CAA iodef alert record
through the Cloudflare API. Renewing the domain itself always happens at
the registrar, since that is a billing action, not a DNS zone record.
Stays in dry run until DRY_RUN=false.
"""
import os
import logging
from datetime import datetime, timezone

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("auto_renewal_payment_failure")

GRACE_STATUSES = {"autorenewperiod", "redemptionperiod", "pendingdelete"}


def evaluate_renewal(expiration_iso, previous_expiration_iso, now_iso, statuses, warn_days=30):
    """
    Pure decision function. No I/O.

    expiration_iso: current RDAP expiration eventDate string.
    previous_expiration_iso: expiration eventDate recorded on a prior run,
        or None if this is the first run.
    now_iso: current time as an ISO string, injected for testability.
    statuses: list of RDAP status strings for the domain.
    warn_days: how many days out counts as inside the warning window.

    Returns a dict:
      days_remaining: int
      stalled: bool, True if the expiration date did not move forward
               since the previous check
      in_grace_period: bool, True if any status flags a lapsed domain
      payment_likely_failed: bool, True when the domain is inside the
               warning window and either stalled or already in a grace
               period, which points at a failed auto-renew charge
    """
    expiration = datetime.fromisoformat(expiration_iso.replace("Z", "+00:00")).astimezone(timezone.utc)
    now = datetime.fromisoformat(now_iso.replace("Z", "+00:00")).astimezone(timezone.utc)
    days_remaining = int((expiration - now).total_seconds() // 86400)

    stalled = False
    if previous_expiration_iso:
        previous = datetime.fromisoformat(previous_expiration_iso.replace("Z", "+00:00")).astimezone(timezone.utc)
        stalled = expiration <= previous

    lowered = {s.lower() for s in statuses}
    in_grace_period = bool(lowered & GRACE_STATUSES)

    inside_window = days_remaining <= warn_days
    payment_likely_failed = in_grace_period or (inside_window and stalled)

    return {
        "days_remaining": days_remaining,
        "stalled": stalled,
        "in_grace_period": in_grace_period,
        "payment_likely_failed": payment_likely_failed,
    }


def fetch_rdap(domain):
    import requests

    r = requests.get(f"https://rdap.org/domain/{domain}", timeout=30)
    r.raise_for_status()
    data = r.json()
    expiration_iso = None
    for event in data.get("events", []):
        if event.get("eventAction") == "expiration":
            expiration_iso = event["eventDate"]
    if not expiration_iso:
        raise ValueError(f"No expiration event found in RDAP response for {domain}")
    return expiration_iso, data.get("status", [])


def find_caa_iodef_record(zone_id, headers, name):
    import requests

    r = requests.get(
        f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records",
        headers=headers,
        params={"type": "CAA", "name": name},
        timeout=30,
    )
    r.raise_for_status()
    for record in r.json().get("result", []):
        if record.get("data", {}).get("tag") == "iodef":
            return record
    return None


def reconcile_iodef_record(zone_id, headers, name, contact_uri, dry_run):
    existing = find_caa_iodef_record(zone_id, headers, name)
    body = {"type": "CAA", "name": name, "data": {"flags": 0, "tag": "iodef", "value": contact_uri}, "ttl": 3600}

    if existing and existing.get("data", {}).get("value") == contact_uri:
        log.info("CAA iodef record already points at %s, nothing to change.", contact_uri)
        return

    if dry_run:
        action = "update" if existing else "create"
        log.info("Dry run: would %s CAA iodef record on %s to %s", action, name, contact_uri)
        return

    import requests

    if existing:
        resp = requests.put(
            f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records/{existing['id']}",
            headers=headers, json=body, timeout=30,
        )
    else:
        resp = requests.post(
            f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records",
            headers=headers, json=body, timeout=30,
        )
    resp.raise_for_status()
    log.info("CAA iodef record on %s now points at %s", name, contact_uri)


def run():
    domain = os.environ["DNS_DOMAIN"]
    zone_id = os.environ["CLOUDFLARE_ZONE_ID"]
    api_token = os.environ["CLOUDFLARE_API_TOKEN"]
    dry_run = os.environ.get("DRY_RUN", "true").lower() == "true"
    contact_uri = os.environ.get("ALERT_CONTACT_URI", "mailto:domains@example.com")
    previous_expiration_iso = os.environ.get("PREVIOUS_EXPIRATION_ISO") or None

    headers = {"Authorization": f"Bearer {api_token}", "Content-Type": "application/json"}

    expiration_iso, statuses = fetch_rdap(domain)
    now_iso = datetime.now(timezone.utc).isoformat()

    result = evaluate_renewal(expiration_iso, previous_expiration_iso, now_iso, statuses)
    log.info(
        "%s expires %s: %d day(s) remaining, stalled=%s, in_grace_period=%s",
        domain, expiration_iso, result["days_remaining"], result["stalled"], result["in_grace_period"],
    )

    if not result["payment_likely_failed"]:
        log.info("OK: %s renewed on schedule, no sign of a failed charge.", domain)
        return

    log.warning(
        "Renewal for %s looks stalled. Auto-renew is likely failing at the payment step. "
        "Update the card or pay manually at the registrar, this cannot be fixed through the DNS provider API.",
        domain,
    )
    reconcile_iodef_record(zone_id, headers, domain, contact_uri, dry_run)


if __name__ == "__main__":
    run()
auto-renewal-payment-failure.js
/**
 * Detect a stalled auto-renewal caused by a failed payment charge, using
 * RDAP as the source of truth, and reconcile the CAA iodef alert record
 * through the Cloudflare API. Renewing the domain itself always happens at
 * the registrar, since that is a billing action, not a DNS zone record.
 * Stays in dry run until DRY_RUN=false.
 */
import { pathToFileURL } from "node:url";

const GRACE_STATUSES = new Set(["autorenewperiod", "redemptionperiod", "pendingdelete"]);

/**
 * Pure decision function. No I/O.
 *
 * expirationIso: current RDAP expiration eventDate string.
 * previousExpirationIso: expiration eventDate recorded on a prior run,
 *   or null if this is the first run.
 * nowIso: current time as an ISO string, injected for testability.
 * statuses: array of RDAP status strings for the domain.
 * warnDays: how many days out counts as inside the warning window.
 *
 * Returns:
 *   daysRemaining: number
 *   stalled: boolean, true if the expiration date did not move forward
 *            since the previous check
 *   inGracePeriod: boolean, true if any status flags a lapsed domain
 *   paymentLikelyFailed: boolean, true when the domain is inside the
 *            warning window and either stalled or already in a grace
 *            period, which points at a failed auto-renew charge
 */
export function evaluateRenewal(expirationIso, previousExpirationIso, nowIso, statuses, warnDays = 30) {
  const expiration = new Date(expirationIso);
  const now = new Date(nowIso);
  const daysRemaining = Math.floor((expiration.getTime() - now.getTime()) / 86400000);

  let stalled = false;
  if (previousExpirationIso) {
    const previous = new Date(previousExpirationIso);
    stalled = expiration.getTime() <= previous.getTime();
  }

  const lowered = new Set(statuses.map((s) => s.toLowerCase()));
  const inGracePeriod = [...lowered].some((s) => GRACE_STATUSES.has(s));

  const insideWindow = daysRemaining <= warnDays;
  const paymentLikelyFailed = inGracePeriod || (insideWindow && stalled);

  return { daysRemaining, stalled, inGracePeriod, paymentLikelyFailed };
}

async function fetchRdap(domain) {
  const res = await fetch(`https://rdap.org/domain/${domain}`);
  if (!res.ok) throw new Error(`RDAP returned ${res.status} for ${domain}`);
  const data = await res.json();
  const event = (data.events || []).find((e) => e.eventAction === "expiration");
  if (!event) throw new Error(`No expiration event found in RDAP response for ${domain}`);
  return { expirationIso: event.eventDate, statuses: data.status || [] };
}

async function findCaaIodefRecord(zoneId, headers, name) {
  const url = `https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records?type=CAA&name=${encodeURIComponent(name)}`;
  const res = await fetch(url, { headers });
  if (!res.ok) throw new Error(`Cloudflare API returned ${res.status}`);
  const data = await res.json();
  return (data.result || []).find((r) => r.data && r.data.tag === "iodef") || null;
}

async function reconcileIodefRecord(zoneId, headers, name, contactUri, dryRun) {
  const existing = await findCaaIodefRecord(zoneId, headers, name);
  const body = { type: "CAA", name, data: { flags: 0, tag: "iodef", value: contactUri }, ttl: 3600 };

  if (existing && existing.data.value === contactUri) {
    console.log(`CAA iodef record already points at ${contactUri}, nothing to change.`);
    return;
  }

  if (dryRun) {
    console.log(`Dry run: would ${existing ? "update" : "create"} CAA iodef record on ${name} to ${contactUri}`);
    return;
  }

  const url = existing
    ? `https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records/${existing.id}`
    : `https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records`;
  const res = await fetch(url, { method: existing ? "PUT" : "POST", headers, body: JSON.stringify(body) });
  if (!res.ok) throw new Error(`Cloudflare API returned ${res.status}`);
  console.log(`CAA iodef record on ${name} now points at ${contactUri}`);
}

export async function run() {
  const domain = process.env.DNS_DOMAIN;
  const zoneId = process.env.CLOUDFLARE_ZONE_ID;
  const apiToken = process.env.CLOUDFLARE_API_TOKEN;
  const dryRun = (process.env.DRY_RUN || "true").toLowerCase() === "true";
  const contactUri = process.env.ALERT_CONTACT_URI || "mailto:domains@example.com";
  const previousExpirationIso = process.env.PREVIOUS_EXPIRATION_ISO || null;

  const headers = { Authorization: `Bearer ${apiToken}`, "Content-Type": "application/json" };

  const { expirationIso, statuses } = await fetchRdap(domain);
  const nowIso = new Date().toISOString();

  const result = evaluateRenewal(expirationIso, previousExpirationIso, nowIso, statuses);
  console.log(
    `${domain} expires ${expirationIso}: ${result.daysRemaining} day(s) remaining, stalled=${result.stalled}, inGracePeriod=${result.inGracePeriod}`
  );

  if (!result.paymentLikelyFailed) {
    console.log(`OK: ${domain} renewed on schedule, no sign of a failed charge.`);
    return;
  }

  console.warn(
    `Renewal for ${domain} looks stalled. Auto-renew is likely failing at the payment step. ` +
    "Update the card or pay manually at the registrar, this cannot be fixed through the DNS provider API."
  );
  await reconcileIodefRecord(zoneId, headers, domain, contactUri, dryRun);
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
  run().catch((e) => { console.error(e); process.exit(1); });
}

Add a test

The part worth testing is the pure decision logic: whether the expiration date actually moved, whether the status flags a grace period, and whether those two facts together mean the payment likely failed. It takes plain values in and returns a plain object, so the tests use fixed timestamps and need no network access.

test_evaluate_renewal.py
from auto_renewal_payment_failure import evaluate_renewal


def test_ok_when_date_moved_forward_and_far_out():
    result = evaluate_renewal(
        "2027-08-05T00:00:00Z", "2026-08-05T00:00:00Z", "2026-07-11T00:00:00Z", []
    )
    assert result["stalled"] is False
    assert result["payment_likely_failed"] is False


def test_stalled_and_inside_window_flags_failure():
    result = evaluate_renewal(
        "2026-08-05T00:00:00Z", "2026-08-05T00:00:00Z", "2026-07-20T00:00:00Z", []
    )
    assert result["stalled"] is True
    assert result["payment_likely_failed"] is True


def test_stalled_but_far_from_window_is_not_yet_a_failure():
    result = evaluate_renewal(
        "2027-08-05T00:00:00Z", "2027-08-05T00:00:00Z", "2026-07-11T00:00:00Z", []
    )
    assert result["stalled"] is True
    assert result["payment_likely_failed"] is False


def test_grace_period_status_always_flags_failure():
    result = evaluate_renewal(
        "2027-08-05T00:00:00Z", None, "2026-07-11T00:00:00Z", ["autoRenewPeriod"]
    )
    assert result["in_grace_period"] is True
    assert result["payment_likely_failed"] is True


def test_no_previous_expiration_defaults_to_not_stalled():
    result = evaluate_renewal(
        "2026-07-20T00:00:00Z", None, "2026-07-11T00:00:00Z", []
    )
    assert result["stalled"] is False
    assert result["payment_likely_failed"] is False
evaluate-renewal.test.js
import { test } from "node:test";
import assert from "node:assert/strict";
import { evaluateRenewal } from "./auto-renewal-payment-failure.js";

test("ok when date moved forward and far out", () => {
  const result = evaluateRenewal("2027-08-05T00:00:00Z", "2026-08-05T00:00:00Z", "2026-07-11T00:00:00Z", []);
  assert.equal(result.stalled, false);
  assert.equal(result.paymentLikelyFailed, false);
});

test("stalled and inside window flags failure", () => {
  const result = evaluateRenewal("2026-08-05T00:00:00Z", "2026-08-05T00:00:00Z", "2026-07-20T00:00:00Z", []);
  assert.equal(result.stalled, true);
  assert.equal(result.paymentLikelyFailed, true);
});

test("stalled but far from window is not yet a failure", () => {
  const result = evaluateRenewal("2027-08-05T00:00:00Z", "2027-08-05T00:00:00Z", "2026-07-11T00:00:00Z", []);
  assert.equal(result.stalled, true);
  assert.equal(result.paymentLikelyFailed, false);
});

test("grace period status always flags failure", () => {
  const result = evaluateRenewal("2027-08-05T00:00:00Z", null, "2026-07-11T00:00:00Z", ["autoRenewPeriod"]);
  assert.equal(result.inGracePeriod, true);
  assert.equal(result.paymentLikelyFailed, true);
});

test("no previous expiration defaults to not stalled", () => {
  const result = evaluateRenewal("2026-07-20T00:00:00Z", null, "2026-07-11T00:00:00Z", []);
  assert.equal(result.stalled, false);
  assert.equal(result.paymentLikelyFailed, false);
});

Case studies

Reissued card

The bank swapped the card number and nobody updated the registrar

A small agency's main domain had auto-renew on for four years without a problem. Then the bank reissued a new card number after an unrelated fraud alert on a different purchase. The registrar's renewal attempt failed twice over the next week, and the account's only notice was an email that landed in a folder nobody checked.

A scheduled RDAP check caught the stalled expiration date seventeen days out. The team updated the card in the registrar's billing page, retried the charge manually, and the expiration date jumped forward the same afternoon.

Fraud block

The renewal the bank mistook for fraud

A side project's yearly renewal charge got auto-declined by the cardholder's bank, which flagged the once-a-year charge from an unfamiliar merchant as suspicious. Auto-renew genuinely tried, twice, and both attempts were blocked before they ever reached the registrar's side of the transaction.

The owner only found out because a scheduled check flagged the domain as stalled nine days before expiry. A quick call to the bank to allow the charge, followed by a manual retry at the registrar, fixed it well ahead of the deadline.

What good looks like

Once a scheduled RDAP check is comparing the real expiration date against what it saw last time, a silently failed renewal charge gets caught days or weeks ahead of the deadline instead of the moment the site goes dark. Keep auto-renew on with a valid, current payment method as the primary defense, and treat the scheduled check as the backup that catches it the moment the charge quietly fails.

FAQ

Why is my domain still about to expire when auto-renew is turned on?

Auto-renew only works if the charge to the payment method on file actually succeeds. If the card expired, was declined, or the bank blocked the charge as suspicious, the renewal attempt fails silently and the domain keeps counting down toward its real expiration date at the registry, even though the auto-renew toggle still shows as on.

Can I fix a failed auto-renewal charge by changing a DNS record?

No. The failed charge itself is a billing event at the registrar, not a DNS zone record, so there is no A, MX, or TXT record that fixes it. You have to update the payment method or pay the renewal manually in the registrar account. What a DNS zone script can do is watch the expiration date and status over RDAP and make sure a CAA iodef alert record is in place so failures get noticed automatically.

How do I know the payment actually failed instead of just being delayed?

Check the registrar account for a billing or renewal notice, since most registrars log a failed charge attempt there even when the reminder email is missed. Cross check with RDAP: if the expiration date has not moved forward and the domain is inside its warning window with auto-renew supposedly on, treat that as a payment failure until the registrar dashboard says otherwise.

Related field notes

Citations

On the problem:

  1. ICANN: FAQs for Registrants, Domain Name Renewals and Expiration. icann.org/resources/pages/domain-name-renewal-expiration-faqs
  2. ICANN: Auto-Renew Grace Period, Acronyms and Terms. icann.org/en/icann-acronyms-and-terms/auto-renew-grace-period-en
  3. RFC 9083, JSON Responses for the Registration Data Access Protocol (RDAP). datatracker.ietf.org/doc/rfc9083

On the solution:

  1. RFC 8659: DNS Certification Authority Authorization (CAA) Resource Record, the iodef tag. rfc-editor.org/rfc/rfc8659.html
  2. RDAP.ORG, the bootstrap lookup service. about.rdap.org
  3. Cloudflare API: DNS records, CAA record model. developers.cloudflare.com/api/resources/dns/subresources/records/models/caa_record

Stuck on a tricky one?

If you have a DNS, domain, or registrar automation problem you would rather hand off, this is the kind of work I do. Message me and we can work through it together.

Contact me on LinkedIn

Did this catch a stalled renewal for you?

If this helped you catch a failed charge before the domain went dark, you can buy me a coffee. It is the best way to keep these field notes free and growing.

Buy me a coffee on Ko-fi

Back to all DNS and Domains field notes