# Chat & profiles

raresats.net has a chat, at [/chat](https://raresats.net/chat), and display names for addresses. Anyone can read. Posting and naming need a wallet that signs **BIP-322** messages (Xverse, UniSat, or your own key): there are no accounts and no passwords, only signatures.

## Topics

The chat is one room for now: **General**. [/chat](https://raresats.net/chat) shows its messages directly; `/chat/general` works too, and a link to any other topic (`/chat/market`, from the first weeks' topics) goes to `/chat`, its `?thread=` and `?m=` kept.

Topics stay in the API, so more can come back: when there is more than one, each has its page, `/chat/<topic>`, its own messages and threads, and the page lists them down the left (on a phone, `/chat` is the list). The first weeks' other topics (Market & prices, Palindromes and the rest) are archived: hidden and closed to posts. Their messages are kept, and a link to one still opens its thread, read only.

- `GET /api/chat/topics` lists them in order, each with `last_message_id` and `last_activity_at` (just General for now). The page keeps the last message id you read in each topic (in your browser only) and marks a topic unread when its `last_message_id` is larger; the header's **Chat** link has a dot while one is.
- `GET /api/chat/messages?topic=market` lists a topic's messages; `POST /api/chat/messages` takes `{text, topic}`. Without `topic`, both are General's (`main`, the one room before topics, is General too: its messages are General's now).
- Only moderators create topics (`POST /api/chat/topics`) and archive them (`DELETE /api/chat/topics/{id}`: hidden and closed to new messages; the messages stay). A topic's id is its URL: 1 to 32 of `a-z`, `0-9` and inner hyphens. Creating one of the first weeks' archived topics brings it back, its messages with it.

## Links to messages

Every message's time links to it: `/chat?m=<id>` (`/chat/<topic>?m=<id>` while there are topics) scrolls to the message and highlights it, and a reply's link, `/chat?thread=<root>&m=<id>`, opens its thread. `?thread=<id>` alone opens a thread. A link naming the wrong topic finds the right one: `GET /api/chat/messages/{id}/replies` answers any message's thread with its `topic`.

## Signing in

1. `GET /api/session/nonce?address=<address>` returns a `nonce`, usable once, for 10 minutes, by that address only.
2. Sign `raresats.net login <address> <nonce> <unix seconds>` with the address (BIP-322 simple or full).
3. `POST /api/session` with `{address, message, signature}`.

The answer sets the session cookie `__Host-rs_chat`: HttpOnly, Secure, SameSite=Lax, Path=/, for **30 days**. The server keeps only a hash of its token. `GET /api/session` says who is signed in; `DELETE /api/session` signs out.

The site signs in with your wallet's ordinals address by default; the payment address works too. Whichever address signs is the one shown in chat.

## Posting

`POST /api/chat/messages` with `{text, topic}` and the cookie. Messages are **plain text**:

- normalized to NFC; control, zero-width, bidi-override and private-use characters are dropped, and line breaks become spaces, so a message cannot spoof its direction or hide text;
- 1 to 500 characters after that;
- shown as text only: never HTML or Markdown, no images. Links are shown as links that open in a new tab, marked `nofollow ugc`, labelled with their real host. A sat number written `sat 1953421509541`, or a bare number of 10 to 16 digits up to the last sat, links to its [sat page](https://raresats.net/sat/1953421509541).

Limits: one message every 3 seconds and 20 a minute per address, and the same text twice within 2 minutes is refused (`duplicate`); the whole chat takes at most 5 messages a second (`room_busy`). Per IPv4 address or IPv6 /64: 20 sign-ins and 10 display-name changes an hour. Messages are kept for 30 days, at most 10,000 a topic.

## Replies and threads

Send `{text, reply_to: <id>}` to reply to a message; the reply is in its root's topic (a `topic` sent with it must be that one). Threads are **one level deep**: a reply to a reply joins its root's thread, and keeps the reply it answered as its `quote` (text only, cut at 140 characters). Every message has `reply_to` (its root, or null on a root), and a root has `reply_count` and `last_reply_at`.

- `GET /api/chat/messages` lists a topic's roots only, each with its thread's counts.
- `GET /api/chat/messages/{id}/replies` lists a thread: its `root` and its replies, newest first, paged like a topic.
- A reply's answer, and its live `chat` event, carry the root's new counts as `thread: {id, reply_count, last_reply_at}`.

Deleting a root deletes its whole thread: replies read wrong without what they answered, and what a moderator deletes is often what they quote. Bans, mutes and deletions apply to replies like any message.

## Reading and live updates

`GET /api/chat/messages` pages newest first; pass the answer's `next` as `before` for older messages. New messages and replies, deletions, topics and name changes are pushed on [the event stream](https://raresats.net/docs/site-feeds.md#live-events) as `chat` and `profile` events: `{type: "message", topic, message, thread?}`, `{type: "delete", ids, threads, topics}` (the roots whose replies went, with their new counts, and the topics touched) and `{type: "topic", topic, archived}`. A page showing another topic marks the message's topic unread.

## Display names

Sign `raresats.net profile <address> display_name=<name> <unix seconds>` and send it to [Set a display name](https://raresats.net/docs/chat-profiles.md#set-a-display-name) with `{address, display_name, message, signature}`. **Each change is signed on its own**: a chat session is not enough. An empty name clears it.

| Rule | |
| --- | --- |
| Length | 3 to 24 characters, trimmed, inner spaces collapsed |
| Characters | letters, digits, spaces and `_ . -`; starts and ends with a letter or digit |
| Unique | compared without case, separators or look-alike digits: `Rare_Hunter` is `rare hunter`, `B0B` is `bob` |
| Not an address | nothing starting `bc1`, no base58 address-like strings, hex blobs or `..` (a shortened address) |
| Not reserved | raresats, admin, moderator, support, official, satoshi and the like, anywhere in the name |
| How often | at most one change in 24 hours |

A name given up stays its last owner's for 7 days.

## Moderation

Chat is on only where the operator turned it on with moderation: AI moderation, moderators (admins), or both. `GET /api/session` says whether the AI moderation is on (`moderated`).

**AI moderation.** Every message and every new display name is checked by a language model before it is posted or taken, against the chat's rules. Refused: harassment and hate, sexual content, threats and violence, doxxing and personal data, scams and phishing (fake support, "send BTC to ...", asking for a seed phrase or private key, wallet-drainer and claim links, posing as raresats.net staff), spam and flooding, malware links, illegal goods. Allowed: trading talk, prices, sat numbers, addresses and txids in context, links to well-known explorers and marketplaces, mild profanity and criticism.

| Answer | Meaning |
| --- | --- |
| 422 `moderated` | Refused; `category` says why. Nothing was posted. |
| 403 `muted` | Three refusals in 10 minutes mute the address for an hour; six from one IPv4 address or IPv6 /48 within an hour pause that network. `until` is when it ends. |
| 429 `rate_limited` | At most 6 texts a minute are checked per address and per IPv4 address or IPv6 /48. |
| 503 `moderation_unavailable` | The text could not be checked right now (timeout or model error). Nothing was posted: try again. |
| 429 `chat_busy` | Too many texts are being checked across the site at once; wait `retry-after` seconds. |

Links must name their real host in plain ASCII: a host with letters from other scripts (a look-alike of a known site), punycode (`xn--`) or `user@host` is refused before the check (422 `suspicious_link`), and the page shows every link by its host name.

The check never fails open: a message that could not be checked is not posted. The rate limits come first, so a refused flood costs nothing. Moderators' own messages are not checked.

**Moderators** sign in like anyone else. Their sessions may delete messages (a thread's root with its replies), remove a display name (it is never given out again) and ban addresses: a banned address cannot post or set a display name, its name is removed, and its recent messages can be removed with the ban. Unbanning also ends a mute. Every moderator action is logged.
