Skip to content

feat(names): put the Veil Names rules behind an HTTP route - #353

Open
Miracle656 wants to merge 5 commits into
mainfrom
feat/names-api
Open

Miracle656 wants to merge 5 commits into
mainfrom
feat/names-api

Conversation

@Miracle656

Copy link
Copy Markdown
Owner

src/names/pricing.ts and src/names/term.ts have held the naming rules —
what a name may be, what it costs, how a term and its expiry work — with no
route to reach them. The wallet's /names page calls
GET {WRAITH_URL}/names/{name} and got a 404 from a server that already knew
the answer.

Those modules also sat on feat/veil-names-pricing mixed with unrelated
retention, offramp, metrics and CI work. This branch cherry-picks only the four
names commits onto main, so none of that rides along.

Routes

route answers
GET /names/price-table bands, term bounds, recovery window — from the rules alone
GET /names/{name} reserved and the refusal reasons
GET /names/{name}/quote?years=N the total, with the term validated not trusted

The split that matters

What these rules can answer, versus what needs a ledger.

With no registry deployed, a valid name returns 503 with
status: "unknown", never a 404
. A 404 reads as nobody has this name —
the false "available" that would let the page sell something nobody can
deliver. "Available" is the one answer that costs somebody money if it is
wrong.

The wallet already renders any non-OK reply as Not live yet and refuses to
offer a claim, so this is handled end to end on both sides.

VEIL_NAMES_CONTRACT_ID deliberately has no default — a placeholder id would
make every answer sound authoritative about a contract that does not exist.

Three CI guards caught real omissions

All three now satisfied, and each one was a genuine gap rather than noise:

  • openapiCoverage — every mounted route must appear in the spec. All
    three paths registered; openapi.json, docs/openapi.json and
    clients/react-query/src/schema.d.ts regenerated.
  • envValidation — every env var read by source must be in .env.example.
  • dockerCompose — every key .env.example documents must be declared in
    docker-compose.yml.

Verification

  • npx tsc --noEmit — clean
  • Full suite — 90 suites, 1409 tests passed
  • src/api/names.ts — 100% statements, 100% functions, 90.9% branches

Related

The registry contract landed in Miracle656/veil#1328 and is now deployed to
testnet at CBR5Z7JN6IGNQNN3IJQMKFMVRESNGQCD2WO2BZPBRTYYRFR2GRHLZXVM,
where register, resolve, payment transfer and the taken/reserved refusals are
all verified on-chain.

https://claude.ai/code/session_01USgemLt4Rnz4SGB1Srf3GB

Length pricing, shortest dearest: 3 characters 10 USDC, 4 characters
5 USDC, 5 and over a flat 2 USDC.

The validation is the load-bearing half. A name resolves over SEP-2 to an
address somebody sends funds to, so two strings a human reads as the same
name must not be two names: case is folded, and non-ASCII is refused
rather than folded, because "jack" with a Cyrillic a is a payment
redirect and not a typo. One- and two-character names are withheld rather
than priced — no contract here is upgradeable, so a name sold is sold
permanently, and holding the scarcest 1,332 of them costs nothing.

Reserved names are refused with hyphens stripped too: without that,
s-u-p-p-o-r-t is a 13-character name at the cheapest tier that reads as
support in every screen that renders it.

Pure module — no database, no payment, no issuance. The claim flow, the
KYC gate and the mint call into this.

Claude-Session: https://claude.ai/code/session_01USgemLt4Rnz4SGB1Srf3GB
Replaces the per-length table. The curve is deliberately not monotonic:
the dearest band is the middle one, because real first names are six to
ten characters and that is where the asking will be. It prices demand,
not scarcity, which is the opposite of the usual registry shape — so the
tests pin the inversion and the comment says not to "fix" it.

One- and two-character names are priced by the 1-5 band but stay
unsellable: 1,332 of them exist, nothing here is upgradeable, and
withholding is reversible where selling is not. MIN_SELLABLE_LENGTH is
the one line that opens them.

Claude-Session: https://claude.ai/code/session_01USgemLt4Rnz4SGB1Srf3GB
The Veil Names design tells the user "3 to 20 characters: lowercase
letters, numbers and underscores" in the field's own help text, so those
are the rules a client will already have promised by the time anything
reaches this module. Max length drops 32 to 20 and the separator moves
from hyphen to underscore.

Underscore is now the ONLY separator. Allowing both would make jack_ng
and jack-ng two different names nobody can tell apart read aloud, which
is the same confusable problem the charset rule exists to prevent.

The reserved check strips underscores for the same reason it stripped
hyphens: s_u_p_p_o_r_t poses as support just as well in a screen that
renders it, and the design promises that names posing as Veil or its
team are reserved.

Prices are unchanged and still the ones given in chat, which disagree
with the design's own table. Flagged, not silently reconciled.

Claude-Session: https://claude.ai/code/session_01USgemLt4Rnz4SGB1Srf3GB
Names are rented by the year, so the prices in pricing.ts become annual
rates and priceRegistration is the one place a multi-year total is
computed — a caller multiplying the rate itself is a caller that can get
the term wrong.

Lifecycle in term.ts. Resolution stops at the instant of expiry with no
grace, as asked: a name that still resolves after its term is a name
whose holder believes it is working.

The part that was my call is the 90-day recovery window after expiry.
Rental creates a hazard permanent ownership did not — a lapsed name taken
by somebody else keeps working in every contact list that has it and
starts paying a stranger. During the window the name is already dead to
senders, but only the previous holder may re-register, which buys two
things at once: someone who forgot a 3 USDC renewal gets their name back
instead of losing it to whoever was watching, and every stale contact
fails for a full quarter before a repoint is even possible.

Renewal is open to anyone, also my call. It only ever extends a name for
its current holder and cannot move or re-point it, so the worst a
stranger can do is pay your bill — and refusing that would mean a name
lapsing because its holder lost their phone in the wrong week.

A year is a flat 365 days, not a calendar year: the contract, the server
and the client must compute one expiry, and "a year from 29 February" has
no single answer.

Claude-Session: https://claude.ai/code/session_01USgemLt4Rnz4SGB1Srf3GB
`src/names/pricing.ts` and `src/names/term.ts` have held the naming rules —
what a name may be, what it costs, how a term and its expiry work — with no
route to reach them. The wallet's /names page calls
`GET {WRAITH_URL}/names/{name}` and got a 404 from a server that already knew
the answer. The modules also sat on a branch mixed with unrelated retention,
offramp and metrics work; this lands just the four names commits.

Three routes:

- `GET /names/price-table` — the bands, the term bounds, the recovery window.
  Answerable from the rules alone.
- `GET /names/{name}` — `reserved` and the refusal reasons, again from the
  rules. Whether a *valid* name is taken needs the registry contract.
- `GET /names/{name}/quote?years=N` — the total. `priceRegistration` validates
  the term rather than trusting it, and is the only place a total is computed:
  a caller that multiplies a per-year price by its own term is a caller that
  can get the term wrong.

The split that matters is between what these rules can answer and what needs a
ledger. With no registry deployed, a valid name returns **503 with
`status: "unknown"`, never a 404**. A 404 reads as "nobody has this name",
which is the false "available" that would let the page sell something nobody
can deliver — and "available" is the one answer that costs somebody money if
it is wrong. The wallet already renders any non-OK reply as *Not live yet* and
refuses to offer a claim, so this is handled end to end on both sides.

`VEIL_NAMES_CONTRACT_ID` has deliberately no default, because a placeholder id
would make every answer sound authoritative about a contract that does not
exist.

Three CI guards caught real omissions rather than noise, and all three are now
satisfied: `openapiCoverage` (every mounted route must appear in the spec, so
all three paths are registered and `openapi.json`, `docs/openapi.json` and
`clients/react-query/src/schema.d.ts` are regenerated), `envValidation` (every
env var read by source must be in `.env.example`), and `dockerCompose` (every
key `.env.example` documents must be declared in `docker-compose.yml`).

`npx tsc --noEmit` clean; full suite 90 suites / 1409 tests passed.

Claude-Session: https://claude.ai/code/session_01USgemLt4Rnz4SGB1Srf3GB

This branch has not been deployed

No deployments
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