input · the_problem TXT

2026 — Design & Full-Stack Build (solo)

Proof you can watch

// MOST VERIFICATION SAYS "CHECK BACK IN 48 HOURS". THIS ONE ASKS YOUR NAMESERVERS AND SHOWS EVERY STEP AS IT LANDS.

ownership_data DATA
RESEARCH
DESIGN
FRONTEND
BACKEND
SOLO. SEPTEMBER 2026.
domainclaim_view · VERIFY.scn VIEW
VERIFY.scn · the product's home-page demoLIVE — REAL PRODUCT CODE, SYNTHETIC DATA
txt · what_goes_wrong TXT

DomainClaim lets you claim a domain and prove you control it with one DNS TXT record. Most products stop at "add this record and check back in 48 hours". This one asks the domain’s own nameservers and shows the check as five steps as they land: find the zone, reach the nameservers, find the record, match the value, record the claim.

Every failure has a name, a reason and one next action. Ownership keeps being checked after it is proved: a name whose record stops answering moves to At risk, and back when the record returns.

What goes wrong

I started with the ways verification fails, before any screen. A resolver that looked too early caches "no record" for up to an hour. DNS panels add your domain to the name you type, so the record lands at the wrong name. A value gets pasted with an end missing. The record is saved as the wrong type. A wildcard answers with another service’s record. One nameserver is down. The token runs out.

Each of these needed its own answer, and each one shaped the product.

research_in DATA
  • IETF domain verification draftA scoped underscore label, the expiry inside the value, and asking the zone’s own servers.
  • RFC 2308How long a resolver keeps saying "no record" after it looked too early.
  • RFC 8555 DNS-01How certificate authorities prove control of a domain.
  • RFC 8552Why an underscore label can’t collide with a real hostname.
  • Five productsHow Google, GitHub, Vercel, Atlassian and Cloudflare verify, and what their errors say.
  • A deployed probeDirect queries from the host, the doubled name, split TXT values and timeouts, measured.
decision_01 · ask_the_source TXT

Ask the source

The check asks the zone’s authoritative nameservers directly. They don’t cache, so a record is found as soon as the DNS host publishes it. I measured that from a deployed function before building on it. There is no "propagation" copy anywhere in the product.

decision_02 · whose_move TXT

Whose move is next

The first check on every new claim looks for a record nobody has added yet. A red cross there would report the product working correctly as a fault. So every step sorts by whose move is next: yours (amber), time’s (cyan), or done (green). There is no red.

decision_03 · claim_then_prove TXT

Claim, then prove

Claiming a name and proving control are separate. A pending claim holds nothing, so anyone can start one. One account holds a name at a time, and the database enforces it with a unique index over the holding states, so a race can’t get around it.

failure_explorer · DEMO_NAMES.scn VIEW
DEMO_NAMES.scn · pick a failureLIVE — REAL PRODUCT CODE, SYNTHETIC DATA
YOUR MOVE
WAITING
PASSES
PLAYING THROUGH ▸

WHAT THE CODE DECIDED

Waiting for the first check on value-mismatch.test

WHERE IT CAME FROM

value-mismatch.test
A partial paste, or a record left over from an earlier claim. The value found is shown above the one to use, with the difference marked.
Friction log, Sep 14: the value could not be read against the panel.
TESTS evaluate.test.ts · difference.test.ts
txt · built_twice TXT

Built twice

I built the engine first: the DNS walk, the typed failures, one owner per name, and a demo namespace where every failure is reachable. The screens got the time that was left, and the stack shaped them into three separate pages.

The first people outside to use it found the problems in the frame. The sign-in link needed a second click, and the layout shifted between screens. Both were on my list, and I had deferred both. So I kept the engine and started the design again from a blank page, in HTML prototypes.

before · first_pass VIEW
// THE FIRST PASS, RUNNING FROM ITS OWN CODE SLOT RESERVED
concepts · 01_02_03 VIEW
Concept 02, scrolling timeline: the claim screen at desktop width
PICKED OPEN THE PROTOTYPE ↗
txt · one_change_at_a_time TXT

I drew three directions from how DNS fails and where the person acts or waits: a rail with a drawer, a scrolling timeline, and the claim as a patch network. The timeline won. It carried the check as one horizontal row of steps on a single cable.

The next version applied a whole list of changes at once and lost what made the timeline work. From then on each prototype changed one thing, and I decided each change before it was made. The design converged in a day.

Prototype 04: every change applied at once, with collapsed cards and a pin button
04 · EVERYTHING AT ONCE
Prototype 05: restarted from the timeline, one change
05 · BACK TO 02, ONE CHANGE
txt · only_real_states TXT

Only real states

Before choosing a stack I checked every state in the design against the backend’s real types. Anything the app couldn’t reach was cut or marked as future work. "Just registered" and "no nameservers set" look the same in DNS, so they became one state. A promise in the copy became build work, or the copy changed.

The stack came last, with one rule: pages never load data, and screens fetch from the API. The check streams its real steps. It never animates a sequence over one finished answer.

Every claim state in the prototype, laid out after the audit against the backend's types
THE STATE AUDIT · PROTOTYPE 15
rules_out DATA
  • Waiting is never drawn as a fault.
  • Cables only where there is a sequence, and straight.
  • Past cards dim. Future cards are absent.
  • The pulse runs only where the check is.
  • Copied is a word, never a tick.
  • Every state shown is one the app can reach.
  • On a phone, a row that scrolls sideways never wraps.
txt · for_the_code_reader TXT

The code is built to be read. DNS sits behind an interface with a scripted fake, so no test touches the network. Failures are values in a typed union, and the message switch is exhaustive: a new failure breaks the build until it has words. An RFC leads the code, and a friction log records every problem I hit using it against real domains. The unit tests cover the pure layers, and the README names what they don’t.

export type FailureReason =
  | { code: 'record_not_found'; queriedName: string;
      nameservers: string[]; negativeTtlSeconds: number | null }
  | { code: 'no_txt_at_name'; queriedName: string }
  | { code: 'appended_zone_suspected'; queriedName: string; foundAt: string }
  | { code: 'value_mismatch'; expected: string; found: string[] }
  | { code: 'token_expired'; expiredAt: Date }
  | { code: 'nameservers_unreachable'; attempted: string[]; timeoutMs: number }
  | { code: 'zone_not_found'; walked: string[] };
src/lib/claims/state.ts · every failure the check can report
read_first DATA
  • src/lib/dns/trace.tsAsks DNS and reports what each server said.
  • src/lib/claims/evaluate.tsCompares that with the claim and decides what the row should do.
  • src/lib/claims/steps.tsTurns a check into the five steps.
  • src/lib/claims/messages.tsEvery failure as title, value, action and reason.
  • src/lib/dns/testNames.tsScripts the demo names.
REPO · AVAILABLE LATER
out · outcome TXT

DomainClaim is live, and the demo names reach every failure above. Designed and not built yet: checking on a schedule while nobody has the claim open, the grace window and the email behind it, transfers between accounts, and a second vantage point.

▸ VISIT LIVE · domainclaim-pi.vercel.app ↗
out · next_network DATA
EVERY FAILURE HAS AN ANSWER.

NEXT ▸ futurity_engine
watch the machine think