Repository navigation
Domain expiration + expired-domain finder via APIVerve - #52
Merged
Merged
Conversation
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.
Deploying with
|
| 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.
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.
…expiration # Conflicts: # src/client/navigation/items.ts
This was referenced Aug 20, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.mdPlans:
docs/superpowers/plans/2026-08-20-apiverve-domain-expiration-phase1.md,…-expired-domain-finder-phase2.mdThe 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
daysToExpirationat 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_CREDITSdeliberately 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
domainraw while the server function normalized to eTLD+1, soblog.x.comandx.comwere 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.Math.floorsends anything under 24h to0and the rule wasexpired <= 0. Now< 0, which is exact because floor sends anything already past to-1or lower.divby text and could drift onto an ancestor. Anchored on a testid.Verification
pnpm ci:checkclean;pnpm testgreen — 270 files, 2635 tests.e2e/expired-domains.spec.tsneeds 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.tsis skipped unlessAPIVERVE_LIVE=1, so an ordinarytest:e2ecan never bill.Deploy notes
APIVERVE_API_KEYalready set in prod. Without it the surfaces report "not configured" and hide rather than crashing.Known gaps
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.[]harmlessly until then.ExpiredDomainsService.tsif that is too rich.🤖 Generated with Claude Code