ServicesMoniker

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 name or its namespace_id.
  • Three forms of every name - what the user typed, the normalized case-folded canonical, and a confusable_skeleton that folds lookalikes together. Uniqueness is enforced on canonical and skeleton, so paypa1, PayPaI, and Cyrillic pаypal all collide with paypal.
  • Reserve, then confirm - reserve makes the atomic claim and returns a reservation_id with a TTL; nothing is owned until confirm. There is no check-then-insert window to race.
  • The lifecycle runs forward only - active to grace on release, then quarantined, 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 DragonSlayers

mine 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_01HX

Importing 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, confirm is 410 reservation_expired; once the sweeper has reclaimed the name it is 404 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_taken until 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: 0 and quarantine_days: 0 took two ticks to go grace, quarantined, available.
  • bulk-grant skips silently. Entries that fail policy (reserved word, profanity, charset, length) or are already claimed are dropped without an error - compare the returned count against what you sent.
  • An unrecognized --status on audit is ignored, returning every name rather than a 400. Use reserved, active, grace, or quarantined.

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

On this page