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 entrypoint | Returns |
|---|---|
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 collectionEverything 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 name | Displayed as |
|---|---|
WORDS | English |
CHINESE | Chinese |
RUSSIAN | Russian |
ARABIC | Arabic |
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:
| Collection | Max name length | Allowed characters |
|---|---|---|
WORDS | 20 | -, 0–9, A–Z (code points 45, 48–57, 65–90) |
CHINESE | 20 | - and CJK ideographs U+4E00–U+9FFF (code points 45, 19968–40959) |
ARABIC | 20 | - and Arabic letters U+0621–U+064A (code points 45, 1569–1610) |
RUSSIAN | 20 | -, Ё 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:
- Detect. Every collection whose character rules accept the name, in schema order.
- Prefer. If more than one matches, the English one, or the first in the list if English is absent.
- 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| Question | Answer |
|---|---|
| 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
- Create a Token (Dashboard): where the collection chip and the availability tick appear
- Create a Token (CLI): how the script resolves a collection, and how to override it
- Community Tokens: what the name you are choosing is attached to
- Community factory contract: the registry and collection entrypoints in full
- Factory and contracts API:
GET /api/factory, the schema the app reads collections from - Sophia standard library (opens in a new tab):
String.length,String.to_listandChar.to_int, which the name check is built from