Skip to content

Domain expiration + expired-domain finder via APIVerve - #52

Merged
ThinkingSpade merged 16 commits into
mainfrom
feat/apiverve-domain-expiration
Aug 20, 2026
Merged

ThinkingSpade merged 16 commits into
mainfrom
feat/apiverve-domain-expiration

Conversation

@ThinkingSpade

@ThinkingSpade ThinkingSpade commented Aug 20, 2026 •

Copy link
Copy Markdown
Owner

Two features on one branch: the domain expiry lookup (Phase 1) and the expired-domain finder built on top of it (Phase 2).

Design: docs/superpowers/specs/2026-08-20-apiverve-domain-expiration-design.md
Plans: docs/superpowers/plans/2026-08-20-apiverve-domain-expiration-phase1.md, …-expired-domain-finder-phase2.md

The constraint that shaped both

APIVerve has no discovery endpoint — verified against their full catalogue. Every domain API takes a domain you already name. It can filter candidates, never find them. So the finder generates candidates from the project's own niche graph and uses APIVerve only to test them.

Honest ceiling, stated in the UI: this finds domains connected to your link and SERP graph. A food blog linking to a competing vending operator will surface; an unrelated lapsed nutrition domain will not. Reaching those needs a zone-file drop feed (DomCop, ExpiredDomains.net) — a second vendor, deliberately out of scope.

What ships

Phase 1 — expiry lookup. Cached APIVerve client, a Domain Overview card, and a credit-free-vs-DataForSEO SAM tool.

Phase 2 — the finder. A new Expired Domains tab under Research. Candidates come from saved competitors and the domains linking to them; they are merged, stripped of platforms, scored on graph evidence, capped, then checked. Rows say why they are there ("links to 3 of your competitors").

Decisions worth reviewing

Absolute dates cached; day counts recomputed on read. APIVerve computes daysToExpiration at call time, so caching the response whole makes it N days wrong on day N of the TTL — a domain three days from dropping would read as ten. A test seeds the cache, advances the clock a week, asserts the countdown fell by exactly 7.

Relevance is scored from graph evidence, never the domain name. Guessing "nutritionhub.com" is food-adjacent from the string is unfalsifiable. We already know why a domain is a candidate — it links to three competitors, or ranks for your keywords. That is what gets scored.

Two corrections to my own plan, both cost-related. Availability is checked only for domains that came back expired (a live domain can never be registered) — roughly halving the bill. And the planned "free estimate, then confirm" flow was dropped: collecting candidates is itself metered, so that estimate would have spent money while calling itself free. The panel quotes the ceiling from the cap instead.

INSUFFICIENT_CREDITS deliberately not reused for an exhausted APIVerve quota. That code means the customer's balance; this is an operator problem they cannot act on.

Defects self-review caught and fixed here

  • Cache-key split, double billing. The MCP tool passed domain raw while the server function normalized to eTLD+1, so blog.x.com and x.com were two entries and two charges — and the card and SAM silently stopped sharing a cache. Normalization moved into the single choke point every caller shares.
  • A domain expiring later today was branded expired. Math.floor sends anything under 24h to 0 and the rule was expired <= 0. Now < 0, which is exact because floor sends anything already past to -1 or lower.
  • The MCP tool advertised "does NOT charge DataForSEO credits" — an agent reads that as free. It costs 5 APIVerve credits.
  • The e2e card locator matched a bare div by text and could drift onto an ancestor. Anchored on a testid.

Verification

  • pnpm ci:check clean; pnpm test green — 270 files, 2635 tests.
  • Live-verified against the real API. deliotx.com: expires 2028-02-22, 551 days out, healthy, 2.5 years old. One live call served two reads; the cache stored only the four absolute dates; the countdown fell 551 → 544 over a simulated week-old entry.
  • e2e/expired-domains.spec.ts needs no API key and no guard, so the property that protects the bill — panel mounts idle, quotes its ceiling, spends nothing until clicked — is checked on every CI run.
  • e2e/domain-expiration-live.spec.ts is skipped unless APIVERVE_LIVE=1, so an ordinary test:e2e can never bill.

Deploy notes

  • No D1 migration. KV only.
  • APIVERVE_API_KEY already set in prod. Without it the surfaces report "not configured" and hide rather than crashing.

Known gaps

  • No run persistence yet. Results are not written to analysisRuns, so there is no restore. That is the safe direction — restore is the known seam where this codebase leaks money — but it means re-running costs again.
  • The SERP-rivals source is built and tested but not wired. It needs a keyword seed list, and which keywords should drive it (saved, tracked, top-GSC) is a product call. It returns [] harmlessly until then.
  • Cap defaults to 50 = up to 250 credits per sweep. One constant in ExpiredDomainsService.ts if that is too rich.

🤖 Generated with Claude Code

ThinkingSpade and others added 10 commits August 20, 2026 03:16
APIVerve is a per-domain lookup with no discovery endpoint, so it can filter
candidate domains but never find them. The spec pins the candidate generator to
the project's own niche graph behind a swappable CandidateSource interface, and
records the correctness rules the implementation has to hold: cache absolute
dates and recompute day-counts on read, own the status thresholds rather than
trusting an undocumented string, and never auto-retry a metered call.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Seven TDD tasks. The file split is dictated by a constraint this repo has been
bitten by before: a module that statically imports cloudflare:workers cannot be
imported by the node-environment Vitest suite, so the thresholds, the transport
and the cache codec each take their dependencies as parameters and only the
server function touches the Workers env. That is what makes the cache-drift
test -- same facts, clock advanced seven days, seven fewer days remaining --
possible to write at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Also lifts domainTargetSchema out of domain-analytics-tools.ts into
mcp/schemas.ts, where projectIdSchema already lives, so both domain tools
reject the same inputs with the same message.
The card sits behind hasData, which normally needs a paid domain-overview
run. VITE_E2E_DOMAIN_FIXTURES makes that overview free, and getFixtureOverview
echoes back whatever domain it is handed -- so the page renders a free overview
for a REAL domain while the card resolves that domain's genuine registration
data. Cost is one APIVerve call and zero DataForSEO credits.

Skipped unless APIVERVE_LIVE=1, so an ordinary test:e2e can never bill.
Errors route through getStandardErrorMessage rather than a fixed string, so
an unset key, a rejected key and a spent APIVerve quota each surface their own
remedy instead of collapsing into one generic failure.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 20, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
flyrocketseo 6f2bf76 Aug 20 2026, 01:44 PM

The MCP tool passed its raw `domain` argument straight to the resolver while
the server function normalized to eTLD+1 first, so `blog.deliotx.com` and
`deliotx.com` produced two cache keys and two billed calls for one registrable
domain -- and the card and SAM silently stopped sharing a cache, which is the
property that was supposed to make repeat lookups free.

Registration belongs to the registrable domain; a subdomain has no expiry of
its own. Normalizing at the single choke point every caller shares makes the
split unrepresentable rather than a rule each new caller has to remember.
wholeDaysBetween floors, so a domain with 20 hours left lands on 0 and the
`expired <= 0` rule called it gone -- alarming, and wrong: it is still live and
still renewable. The boundary moves to `< 0`, which is exactly right because
floor sends anything already in the past to -1 or lower (an hour ago is
floor(-0.04) = -1). 0 now means "expires later today" and reads as critical.

Also: the MCP tool advertised "does NOT charge DataForSEO credits", which an
agent reads as free -- it costs 5 APIVerve credits on a miss, so say that. And
the e2e card locator matched a bare div by text, which could drift onto an
ancestor and make `not.toContainText` read the whole page; it is anchored on a
testid now.
Prices the sweep honestly: the spec assumed expiry AND availability for every
candidate, but availability only matters for a domain that already lapsed --
a live domain is never registerable. Checking it only on the expired subset
roughly halves the cost, 250 credits at the default cap of 50 rather than 500.
…scoring

Three pure pieces the finder needs, none of them wired to a UI yet.

Availability answers a different question from expiry and is the one that
actually matters for acquisition: a lapsed domain sits in redemption for ~75
days and is not registerable in that window. Unknown stays null rather than
collapsing to false, because rendering "taken" over a failed lookup would bury
exactly the domain this feature exists to surface.

The bulk resolver refuses an over-cap batch instead of truncating it -- a
shortened sweep that looks complete would let the finder report "none expired"
over domains it never checked.

Scoring reads GRAPH EVIDENCE, never the domain name: a candidate is relevant
because it links to three of your competitors or ranks for your keywords, not
because "nutritionhub" sounds food-adjacent.
@ThinkingSpade ThinkingSpade changed the title Domain expiration via APIVerve (Phase 1) Domain expiration via APIVerve (Phase 1, + Phase 2 primitives) Aug 20, 2026
Candidates come from the project's own niche graph -- its saved competitors and
the domains linking to them -- because APIVerve can only test a domain you name.
Each source injects its fetcher, so all of it is unit-tested with no network and
no DataForSEO key, and a source that fails is named in the UI rather than
silently shrinking coverage.

Corrects the plan's cost model in two places. Availability is checked ONLY for
domains that came back expired, since a live domain can never be registered --
that roughly halves the bill. And the plan's "free estimate, then confirm" flow
is dropped: collecting candidates is ITSELF metered, so an estimate that had to
collect first would have spent money while calling itself free. The panel quotes
the ceiling from the cap instead, before anything runs.

The e2e spec needs no API key and no guard, so the property that protects the
bill -- mounts idle, quotes the cost, spends nothing until clicked -- is checked
on every CI run rather than only when someone opts into a live test.
@ThinkingSpade ThinkingSpade changed the title Domain expiration via APIVerve (Phase 1, + Phase 2 primitives) Domain expiration + expired-domain finder via APIVerve Aug 20, 2026
…expiration

# Conflicts:
#	src/client/navigation/items.ts
@ThinkingSpade
ThinkingSpade merged commit ef78cae into main Aug 20, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant