Moniker
A naming registry for anything that needs unique handles: guild names, world names, shop names, agent callsigns, workspace subdomains. You define a namespace with its own charset, length, and case rules; Moniker enforces uniqueness atomically, blocks reserved words, profanity, and homoglyph lookalikes, and runs each name through a reserve-confirm-release-quarantine lifecycle.
When to reach for it: any name your users choose that must stay unique and must not be impersonated, especially when releasing one should not hand it straight to a squatter.
When not to: platform usernames and emails are already unique - that is Auth. Attaching an existing external handle (a Discord account, a wallet address) to a user is Alias. A short exclusive claim with no policy or lifecycle behind it is a lock - Distributed State.
Concepts
- The namespace owns the policy - charset, length, case folding, Unicode
normalization, reserved words and patterns, profanity level, and the
lifecycle windows. Endpoints accept its
nameor itsnamespace_id. - Three forms of every name - what the user typed, the normalized
case-folded
canonical, and aconfusable_skeletonthat folds lookalikes together. Uniqueness is enforced on canonical and skeleton, sopaypa1,PayPaI, and Cyrillicpаypalall collide withpaypal. - Reserve, then confirm -
reservemakes the atomic claim and returns areservation_idwith a TTL; nothing is owned untilconfirm. There is no check-then-insert window to race. - The lifecycle runs forward only -
activetograceon release, thenquarantined, then back to the pool, advanced by a background sweeper. A released name is not instantly re-registerable, its owner included.
Creating a namespace
The CLI takes a JSON policy document:
snug moniker namespaces create --file ./guilds.json
snug moniker namespaces list --sort-by name --sort-order asc
snug moniker namespaces get --namespace-id nqeGELzuwbabhRmshhHd{
"name": "guilds",
"policy": {
"charset": "alphanumeric_underscore", "min_length": 3, "max_length": 24,
"case_insensitive": true, "unicode_normalization": "nfkc",
"confusable_detection": true, "profanity_filter": "standard",
"reserved_words": ["admin", "official"], "reserved_patterns": ["^snug_"]
},
"lifecycle": { "reservation_ttl_seconds": 900, "grace_period_days": 30, "quarantine_days": 90 }
}policy, premium, and lifecycle are each all-or-nothing. Omit one and
you get the defaults shown above (with profanity off); include one and every
field must be present - a policy block missing charset is rejected with
400 BAD_REQUEST: missing field 'charset'. Namespace names are unique;
re-creating one is 409 namespace_already_exists.
reserved_words is exact-match on the canonical form (admin blocks
admin, not admins) - use reserved_patterns for prefixes. A base list
(admin, administrator, root, system, official, support, snug)
applies in every namespace on top of yours.
Registering a name
snug moniker check --namespace guilds --name DragonSlayers
snug moniker reserve --namespace guilds --name DragonSlayers
snug moniker confirm --namespace guilds --reservation-id name_85cf3fab...check reserves nothing. It answers with available, a precise reason
when the answer is no (taken, reserved, grace, quarantined,
confusable_with, reserved_word, profanity, invalid), and up to five
suggestions (DragonSlayers_, DragonSlayers2, TheDragonSlayers, ...)
that have themselves been validated against the policy.
confirm is the only step that creates ownership, and the reservation_id
becomes the name's permanent name_id:
curl -X POST -H "Authorization: Bearer $SNUG_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"reservation_id": "name_85cf3fabbc554efa8eab85c03267aed5"}' \
http://localhost:4000/api/v1/moniker/guilds/confirm{
"status": 200, "msg": "OK",
"data": {
"name_id": "name_85cf3fabbc554efa8eab85c03267aed5",
"namespace_id": "nqeGELzuwbabhRmshhHd",
"name": "DragonSlayers", "canonical": "dragonslayers",
"confusable_skeleton": "dragonsiayers",
"status": "active", "premium": false,
"owner": { "type": "user", "id": "docs-wave" },
"registered_at": "2026-08-28T22:53:33.538962Z", "expires_at": null
}
}Only the reserving principal may confirm; anyone else gets
403 insufficient_permissions, and confirming an already-active name is
400 invalid_name. Pass --owner-type organization or agent on reserve
to register for an org or a registered agent instead.
Ownership
snug moniker mine --namespace guilds
snug moniker history --namespace guilds --name DragonSlayers
snug moniker transfer --namespace guilds --name DragonSlayers --to user_01HXBUYER
snug moniker release --namespace guilds --name DragonSlayersmine lists every name the caller owns, grace-status ones included.
release drops the name to grace rather than freeing it. Both transfer
and release are owner-only (403 insufficient_permissions); a platform
admin can act on any name. history is readable by any authenticated caller
in the tenant - public provenance, not a private log - and most transitions
also mirror onto a Timeline stream named
moniker_{namespace_id}. To hold a buyer's funds until the seller signs off,
see Premium names and escrow.
Admin tools
These four need a platform admin token; a normal token gets
403 insufficient_permissions.
snug moniker admin reserved-words --namespace guilds --words moderator,staff
snug moniker admin force-quarantine --namespace guilds --name SuspiciousName --days 365
snug moniker admin bulk-grant --namespace guilds --file ./migration.json
snug moniker admin audit --namespace guilds --status active --owner-id user_01HXImporting reserved words merges and de-duplicates them into the policy and
bumps the namespace version - the only thing that moves it. bulk-grant
takes {"grants": [{"name": ..., "owner_id": ..., "owner_type": ...}]} and
registers names straight into active. audit is the only search across
owners: a full-text q plus status and owner_id filters, not the
shared search grammar.
Behaviors and gotchas
- Two different errors for a late confirm. Past the deadline but before
the sweeper runs,
confirmis410 reservation_expired; once the sweeper has reclaimed the name it is404 reservation_not_found. - Grace is not a reclaim window over the API. There is no reclaim
endpoint; re-reserving your own released name is
409 name_takenuntil quarantine ends. Plan for the full grace plus quarantine wait. - Transitions run on a 60-second sweeper tick, one hop per tick. Even a
namespace with
grace_period_days: 0andquarantine_days: 0took two ticks to go grace, quarantined, available. bulk-grantskips silently. Entries that fail policy (reserved word, profanity, charset, length) or are already claimed are dropped without an error - compare the returnedcountagainst what you sent.- An unrecognized
--statusonauditis ignored, returning every name rather than a400. Usereserved,active,grace, orquarantined.
Limits and configuration
Up to 1000 namespaces, 256-character names, 10000 reserved words and 100
reserved patterns per namespace, 5 suggestions per check, and 1000 names per
bulk grant - tunable via the MONIKER_* variables in the
Moniker CONFIG reference.
Reference
- Moniker API - every endpoint, callable
- Premium names and escrow - priced names, escrowed transfers
- Related: Auth for platform usernames, Alias for external handles, and Treasury for the wallets underneath