Tokens & Bonding Curves
Collections & Naming Rules

Collections & Naming Rules

A collection is a namespace inside the community factory. It owns a character set, a maximum name length, and a registry of the token sales created inside it. Names are unique per collection, not globally, and you never pick a collection explicitly in the app. It is derived from the characters you type.

What a collection defines

Factory entrypointReturns
allowed_name_length(collection_name)Maximum name length the collection accepts
allowed_name_chars(collection_name)The whitelisted character classes
token_sale_registry(collection_name)Map of name to sale contract for everything in the collection
has_community(collection_name, name)Availability check
get_token_sale(collection_name, name)Resolve a name to its sale contract
token_contract(collection_name, name)Resolve a name to its AEX-9 contract
create_collection(collection_name, allowed_name_length, allowed_name_chars)Create a namespace. Payable

The registry enforces two further rules: a community's name must be unique within its collection (NAME_ALREADY_REGISTERED), and the token's name is also its symbol: create_community passes the same name string as both.

Both name checks run inside create_community and abort the whole transaction: NAME_TOO_LONG when the name exceeds the collection's allowed_name_length, and NAME_NOT_ALLOWED_CHAR when any character falls outside its allowed_name_chars.

The collection id

On-chain a collection is identified by a compound string, not a bare name:

<NAME>-ak_<deployer address>

WORDS-ak_2X6…      the English collection

Everything before -ak_ is the collection's own name; the address is the account that created it. create_collection builds the suffix itself:

let collection_name = String.concat(String.concat(collection_name, "-"), Address.to_str(Call.caller))

So two different accounts can both create a collection called WORDS without colliding, and a collection id is not portable between networks.

The UI splits on -ak_ to get the display name and then applies a label override:

On-chain nameDisplayed as
WORDSEnglish
CHINESEChinese
RUSSIANRussian
ARABICArabic

Any other collection is displayed under its own name. WORDS is the default: the app prefers it when a name fits several collections, and the agent skill's create command always uses it.

The live collections

The Superhero backend indexes a configured allowlist of collections. Four are configured on mainnet:

CollectionMax name lengthAllowed characters
WORDS20-, 0–9, A–Z (code points 45, 48–57, 65–90)
CHINESE20- and CJK ideographs U+4E00–U+9FFF (code points 45, 19968–40959)
ARABIC20- and Arabic letters U+0621–U+064A (code points 45, 1569–1610)
RUSSIAN20-, Ё and А–Я (code points 45, 1025, 1040–1071)

None of the three non-Latin collections admits digits or Latin letters, and RUSSIAN admits only uppercase Cyrillic. All four cap names at 20 characters. WORDS was deployed from a different account than the other three on mainnet, so its -ak_ suffix differs.

A collection that exists on-chain but is not in the backend's configuration is not indexed, and tokens created in it do not appear in the app. There is no factory entrypoint that enumerates collections; to discover them from the chain, replay CreateCollection(string, address) events.

The API returns the resolved collection on a token as collection (the full id) and collection_info, which carries id, name, description and allowed_name_length.

How character rules are expressed

allowed_name_chars is a list of Sophia variants, each either a set of single code points or an inclusive code point range:

allowedNameChars = SingleChar(int) | CharRangeFromTo(int, int)

The English collection is three rules:

[ SingleChar([45]), CharRangeFromTo(48, 57), CharRangeFromTo(65, 90) ]

  45        =  "-"
  48 to 57  =  "0" to "9"
  65 to 90  =  "A" to "Z"

The app turns each rule set into a regular expression character class (\-0-9A-Z for the English collection), anchored as a whole-string match with the u flag, so astral code points count as one character rather than as a surrogate pair. A name is valid for a collection when every one of its characters is in that class.

The English rule set does not include lowercase letters, spaces, underscores, dots or emoji. That is why the create form uppercases as you type and rewrites spaces as dashes. That turns near-misses into valid names rather than rejecting them.

How the app picks a collection for you

Because every collection allows -, a name such as A-B is valid in all of them at once. The app resolves this in three steps, re-derived on every keystroke:

  1. Detect. Every collection whose character rules accept the name, in schema order.
  2. Prefer. If more than one matches, the English one, or the first in the list if English is absent.
  3. Honour an explicit choice. If you picked a collection from the chip's dropdown and it still accepts the current name, that choice wins; otherwise the preference takes over again.

If no collection matches, the form shows an Unsupported badge and disables submit, because a name no collection accepts would only burn gas and revert on-chain.

The dropdown appears only when the name fits more than one collection. HELLO is Latin-only and ПРИВЕТ is Cyrillic-only, so for those the chip is just a label.

Name length

The cap comes from the collection: allowed_name_length(collection_name), exposed on the API as collection_info.allowed_name_length. The create form caps its input at 20 characters and shows a counter against 20, which matches all four configured collections.

The cap is in characters, not bytes: Sophia's string functions operate on unicode code points, so String.length("汉字") is 2 and the CHINESE collection allows 20 ideographs. The character check compares each code point against the collection's rules, which is why they can be expressed as ranges like 19968 to 40959.

Creating a collection

create_collection(collection_name, allowed_name_length, allowed_name_chars) is a payable entrypoint on the factory and emits CreateCollection(string, address):

payable stateful entrypoint create_collection(collection_name : string, allowed_name_length : int, allowed_name_chars : list(allowedNameChars)) =
  require(!Map.member(collection_name, state.collection_registry), "collection_ALREADY_EXISTS")
  require(Call.value == 555 * (10 ^ 18), "COLLECTION_CREATION_DONATION_MISMATCH")
  let collection_name = String.concat(String.concat(collection_name, "-"), Address.to_str(Call.caller))
  put(state{ collection_registry[collection_name] = ... })
  Chain.spend(ak_11111111111111111111111111111111273Yts, Call.value)
  Chain.event(CreateCollection(collection_name, Call.caller))
  collection_name
QuestionAnswer
Who may call it?Anyone. There is no owner check, no allowlist and no moderator role.
What does it cost?Exactly 555 AE: Call.value == 555 * 10^18. Attaching more aborts just as surely as attaching less. There is no refund path, because there is no surplus.
Where does the 555 AE go?Chain.spend to ak_11111111111111111111111111111111273Yts, the all-zero account. It is burned, not paid to the platform or to a treasury.

The creator chooses the collection's rules outright: the length cap and the whole character whitelist are arguments. The only uniqueness check is collection_ALREADY_EXISTS, and because the id is suffixed with the caller's address, it only stops the same account creating the same name twice.

The 555 AE matches the fee for opening a DAO proposal and serves the same purpose: it makes namespace spam expensive. A DAO proposal's 555 AE goes to the community treasury; a collection's is burned.

Neither the web app nor the agent skill exposes create_collection; call it directly against the factory. The backend indexes only configured collections, so tokens created in a new collection do not appear in the app until it is added to the configuration.

Checking a name yourself

From the CLI, against the live factory:

node scripts/superhero-token-create.mjs check "MYTOKEN"

Or directly, to test against a specific collection rather than the default:

const exists = (await factory.has_community(collectionId, name)).decodedResult;
const maxLen = (await factory.allowed_name_length(collectionId)).decodedResult;
const chars = (await factory.allowed_name_chars(collectionId)).decodedResult;

has_community is the authoritative check. The create form's live tick searches the token index instead, which is faster but can lag a very recent creation by a few minutes.

Related