NKIP-0005 · Draft
Cashtags on Nostr
Abstract. A note that mentions $XMR can say which asset it means and how the author
wants it shown, with one optional tag. Clients that don't know the tag still
see the plain $XMR text.
| NKIP | 5 |
|---|---|
| Title | Cashtags on Nostr |
| Status | Draft |
| Type | Standards Track |
| Created | 2026-10-27 |
| License | CC0-1.0 |
| Requires | NIP-01, NIP-24, CAIP-2, CAIP-19 |
Files: cashtag.schema.json · vectors/v1.json
Contents
Proposed upstream. Cashtags are general Nostr, not specific to no-KYC services, so this NKIP is proposed to nostr-protocol/nips as a NIP: nostr-protocol/nips#2528. Once that NIP is merged it becomes the reference, and this page will be marked superseded and point to it.
Motivation
People write $TICKER in notes the way they do elsewhere, and clients turn it
into a price chip or chart. Two problems follow:
- Which asset? Tickers aren't unique. Many tokens share a symbol, and a stock and a coin can too. Today each client guesses, so two readers of the same note can show different assets.
- Which display? An author who wants a full chart under the note has no way to say so.
No NIP covers cashtags. NIP-24 t tags are hashtags; NIP-73 external content
ids cover URLs, books, blockchain transactions and addresses, but not assets.
Goals:
- G1 Every client tokenizes
$TICKERin note text the same way. - G2 An author can name the asset exactly, so readers that support it show the same asset.
- G3 The tag never changes what the note says. A reader without it, or one that can't resolve the asset, falls back to the ticker in the text.
- G4 Notes about an asset can be found through ordinary relay filters.
Non-goals:
- Price data, quotes or trading.
- A registry of every ticker. Asset ids name assets; this NKIP doesn't rank or vouch for them.
Roles
| Role | Does |
|---|---|
| Writer | The client that publishes the note. Tokenizes the text, lets the author attach one cashtag, writes the tag. |
| Reader | Any client that renders the note. Tokenizes, reads the tag, resolves the asset or falls back to the ticker. |
| Relay | Stores the note. Indexes single-letter tags only (NIP-01), so it never sees cashtag as a filterable tag. |
Specification
The key words MUST, SHOULD and MAY are used as in RFC 2119.
Cashtags in text
A cashtag is $ followed by 2–6 ASCII letters, where:
- the character before
$is the start of the text or not a word character or$; - the character after the letters is the end of the text or not a word character;
- it is not inside a skipped span.
A word character is _ or any code point in the Unicode general
categories L (letters), M (marks) or N (numbers), in any script. Combining
marks count as word characters: $XMR followed by U+0301 is "XMŔ", not
$XMR, so it isn't a cashtag, and neither is $XMR after a mark. The same
holds for non-ASCII digits ($XMR٣) and other numbers ($XMR²). Text is
taken as it is, without Unicode normalization.
Skipped spans, found in this order and removed before tokenizing:
- fenced code: from
```to the next```, or to the end of the text when unclosed; - inline code:
`followed by one or more characters other than`and newline, then`; - URLs:
http://orhttps://up to the next whitespace; nostr:entities:nostr:followed by lower-case letters and digits.
Readers replace a skipped span with word characters, so a $ glued to one
isn't a cashtag either. Code is skipped because $HOME or $PATH in a shell
snippet is not a ticker.
The ticker is the letters upper-cased: $xmr, $Xmr and $XMR are all
XMR. So $562.82 and $5 are prices, $X is too short, $ABCDEFG and
$XMR2 aren't cashtags, and US$XMR, $$XMR and $XMRé aren't either.
Punctuation, whitespace and emoji are boundaries: ($XMR), $BTC! and
$ETH🚀 are cashtags.
Tickers with digits ($1INCH) or more than six letters aren't cashtags in
v1 (see Open questions).
The cashtag tag
["cashtag", "<TICKER>", "<mode>", "<asset-id>"]
| Element | Writer | Rule |
|---|---|---|
TICKER |
MUST | 2–6 upper-case ASCII letters. It MUST appear as a cashtag in the note's content (per the rules above). |
mode |
MUST | compact (an inline chip, the default) or expanded (a chart or card under the note). |
asset-id |
MAY | Which asset the ticker means (see Asset ids). Leave it out rather than guess. |
A writer MUST emit at most one cashtag tag per event: it is the author's
one attachment, not a list of every ticker in the text. The tag MAY appear on
any event whose content is text for people (kind 1 notes and their replies,
kind 1111 comments, long-form posts).
A reader MUST:
- use only the first
cashtagtag; - upper-case the tag's ticker, and ignore the tag when the ticker is invalid or doesn't appear as a cashtag in the content;
- read a missing or unknown
modeascompact; - ignore an
asset-idthat isn't valid or isn't bound to the ticker (see below) and resolve by ticker alone, as for the three-element form.
The three-element form (no asset id) stays valid in every version.
Asset ids
The fourth element takes one of two forms.
CAIP-19, for example eip155:1/erc20:0xA0b8…eB48. It names an asset on
one chain exactly. It contains /, so it can't be confused with the short
form.
Short form, lower case, :-separated, starting with the ticker:
| Form | Means | CAIP-19 equivalent |
|---|---|---|
<ticker>:native |
The coin of the ticker's own network: the asset itself, not a wrapped or bridged copy | Listed in the table below; otherwise none |
<ticker>:equity |
Shares of the listed company with that primary-market ticker, not one particular tokenized version | None (see Open questions) |
<ticker>:<chain>:<address> |
A token contract or mint on <chain> (table below). The address keeps its case |
<CAIP-2 of chain>/<erc20 or token>:<address> |
A short id is parsed in two steps:
- Shape.
^[a-z0-9]{1,16}:(native|equity)$, or^[a-z0-9]{1,16}:[a-z0-9]{1,16}:[A-Za-z0-9]{1,128}$where the middle part is the chain slug. - Chain. For the three-part shape, the slug MUST be in the chain table below and have a token namespace.
Anything that fails either step (xmr:solana with no address,
xmr:native:abc, upper case, an unknown chain, a chain without tokens) is
ignored by readers.
Ticker binding. The first part of a short id MUST equal the tag's ticker,
lower-cased. A reader ignores ["cashtag","XMR","compact","btc:native"]'s
asset id, so a note can't show $XMR in the text while the card shows a
different coin. A CAIP-19 id carries no ticker, and anyone can
deploy a contract with any symbol, so for CAIP-19 ids and the
<chain>:<address> form a reader that resolves the asset MUST compare the
resolved symbol with the tag's ticker (ignoring case):
- Same symbol: show it as
$TICKER, together with the chain and a shortened address (for example$USDC · Base · 0x8335…2913). A matching symbol proves nothing on its own: whoever deploys a contract also picks its symbol. A reader MAY drop the chain and address only for assets on a list of known contracts it ships or trusts. - Different symbol: show the asset's own name and symbol, for example
"Fake Coin (FAKE)", and mark it as not matching the text. The reader MUST
NOT present that asset as
$TICKERanywhere: not on the chip, the card, or a price line. - Not resolved (unknown, offline): fall back to the ticker alone, as for the three-element form, and SHOULD say the asset couldn't be checked.
native and equity ids need no lookup: the binding check already ties them
to the ticker.
Native assets (normative; additions only through the changelog). The
monero: CAIP-2 reference is the registered one (ChainAgnostic namespaces,
monero/caip2.md: the first 32 characters of the genesis block hash);
slip44:<coin type> is the generic CAIP-19 namespace for native coins.
| Short id | CAIP-19 |
|---|---|
xmr:native |
monero:418015bb9ae982a1975da7d79277c270/slip44:128 |
btc:native |
bip122:000000000019d6689c085ae165831e93/slip44:0 |
eth:native |
eip155:1/slip44:60 |
sol:native |
solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501 |
A <ticker>:native id that isn't in the table is valid but no stronger than
the ticker: the reader resolves it by ticker as the network's own coin.
Chains for the <chain>:<address> form (normative):
| Slug | CAIP-2 | Token namespace |
|---|---|---|
ethereum |
eip155:1 |
erc20 |
optimism |
eip155:10 |
erc20 |
bsc |
eip155:56 |
erc20 |
polygon |
eip155:137 |
erc20 |
base |
eip155:8453 |
erc20 |
arbitrum |
eip155:42161 |
erc20 |
solana |
solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp |
token |
bitcoin and monero are known chains with no token form, so
xmr:monero:<anything> is ignored. Writers SHOULD use CAIP-19 for chains not
listed here.
Identity. The asset id is the asset's identity; the ticker is only how it
is shown. Two notes with $CAT and different asset ids are about different
assets. Readers that group or count notes by asset MUST group by the
CAIP-19 equivalent when there is one, and by the short id otherwise, never by
ticker alone when an id is present.
Finding notes by cashtag
Relays index only single-letter tags (NIP-01), so {"#cashtag": [...]} is not
a filter any relay answers. Writers MAY add a NIP-24 t tag whose value is the
lower-cased cashtag, including the $:
["t", "$xmr"]
$xmr is a different value from the hashtag xmr, so #XMR and $XMR
feeds stay apart, and {"#t": ["$xmr"]} works on every relay today.
A writer that adds them derives them from the content, not from the author:
one per distinct cashtag (per the tokenizer above), in first-seen order,
$ plus the ticker in lower case, at most 10. The value is always exactly
lower case, because relays and clients may lower-case t values (NIP-24
requires it), and a mixed-case value would then be stored or filtered under
a different string than other writers use. Readers filter with the
lower-case value too. Readers MUST NOT treat a t value
starting with $ as an attachment; the cashtag tag alone carries the mode
and asset. Relays with NIP-50 search can also match $XMR in the content.
Why not NIP-73
NIP-73 i tags name external content (a URL, a book, a transaction, an
address) that a note is about. An asset fits that idea, and a future NIP-73
asset type (for example ["i", "<CAIP-19>"] with ["k", "caip19"]) would be
the natural place for G4. v1 doesn't use it:
- NIP-73 defines no asset type today, so any choice would be invented here
and readers of
itags wouldn't know it; - an
itag can't carry the author's display choice (mode), which is half of what thecashtagtag is for; iis single-letter and indexed, and clients already treat anitag as "this note comments on that external thing", which a passing$XMRmention isn't.
When NIP-73 gains an asset type, writers SHOULD add it alongside the
cashtag tag for the attached asset; the cashtag tag stays.
Requirements
| ID | Level | Goal | Requirement | Acceptance check |
|---|---|---|---|---|
| R1 | MUST | G1 | Readers and writers tokenize $TICKER with the boundary, length, ASCII and URL rules above |
Test vectors: tokenize |
| R2 | MUST | G3 | A cashtag tag counts only when its ticker appears as a cashtag in the content; only the first tag counts |
Test vectors: events |
| R3 | MUST | G3 | Missing or unknown mode reads as compact; a bad or unbound asset id is ignored and the reader resolves by ticker |
Test vectors: events |
| R4 | MUST | G2 | A short id's first part equals the ticker; a CAIP-19 or contract asset whose resolved symbol differs is shown by its own name and never as $TICKER; a contract asset always shows its chain and a shortened address unless it is on a known-contract list |
Test vectors: display |
| R5 | MUST | G2 | Short ids map to CAIP-19 by the tables above | Test vectors: native and token mappings |
| R6 | MUST | G2 | Readers group notes by asset id, not ticker, when an id is present | Code review |
| R7 | MAY | G4 | Writers derive ["t","$<ticker lower>"] from the content: per distinct cashtag, first-seen order, exactly lower case, at most 10 |
Test vectors: search |
| R8 | MUST before leaving Draft | — | Schema, test vectors and a checker in CI | CI on this repo |
Security considerations
| Threat | Likelihood | Control |
|---|---|---|
Ticker squatting: a token deployed with a popular symbol, attached as $XMR |
High | A mismatched symbol is never shown as $TICKER (R4). A matching symbol is no proof, so contract assets always show their chain and a shortened address unless they are on the reader's known-contract list (R4) |
Mismatched attachment: text says $XMR, asset id says another coin |
Medium | Short ids are bound to the ticker (R4); the tag's ticker must appear in the content (R2) |
| Hidden attachment: a tag for a ticker the text never mentions, to put a card under an unrelated note | Medium | R2 |
Pump spam through t search tags |
Medium | Capped at 10 per note; readers apply their usual spam and web-of-trust filters to #t feeds |
Lookalike text ($ХMR with a Cyrillic Х) |
Low | Only ASCII letters form a ticker, and a non-ASCII letter next to the letters breaks the cashtag (R1) |
| Price lookups leak interest | Low | A reader fetching quotes reveals which tickers the user views to its price source; readers SHOULD cache, batch and fetch through their usual proxy |
Implementations
Add yours with a pull request. Projects by this NKIP's authors are marked.
| Project | Role | Status |
|---|---|---|
| Ripley Terminal (authors' project) | Writer and reader | Publishes the tag shape, modes and short asset ids this NKIP is taken from |
Open questions
- Tickers with digits (
$1INCH,$SHIB2) or 7–10 letters: allow in v2, at the cost of matching more prose? - Equities: name the share class exactly (ISIN, or MIC + ticker) instead
of
<ticker>:equity? - Propose a CAIP-19 asset type for NIP-73 upstream and switch G4 to it.
- Should
tsearch tags be SHOULD rather than MAY?
Test vectors
nkip-0005/vectors/v1.json, checked by python3 tools/nkip0005.py check
nkips/nkip-0005/vectors/v1.json (standard library only):
- 26 tokenize cases: text → the cashtags it contains, including prices,
length limits, digits, underscores, glued
$, non-ASCII letters, combining marks and non-ASCII numbers on either side, punctuation and emoji, URLs,nostr:entities, inline code, and closed and unclosed fenced code; - 25 events → the attachment a reader shows (or none): the three- and four-element forms, a lower-case ticker with no mode, unknown and wrong-case modes, ticker missing from the content or only inside a URL or code, only the first tag, native, equity, Solana and EVM tokens with their CAIP-19 equivalents, a CAIP-19 id, and ignored asset ids (another ticker, bad shape, unknown chain, chain without tokens, upper case, not a string);
- 4 search cases: content → the derived
ttags, including dedupe, lower-casing and 11 distinct cashtags plus a repeat (10 kept); - 5 display cases with a fixed resolver table in the file (CAIP-19 id → symbol and name, standing in for a real lookup): a matching symbol, a mismatched CAIP-19 symbol, a mismatched contract symbol, a native id, and an unresolved id.
The schema nkip-0005/cashtag.schema.json describes the tag only as writers
emit it. Readers are more lenient (they upper-case the ticker, read a missing
or unknown mode as compact, and ignore a bad asset id); the event vectors
cover that.
Changelog
- 2026-10-27: Contract assets always show chain and short address (a matching symbol is no proof); Monero CAIP-2 source noted; Implementations section.
- 2026-10-27: Draft. The tag shape, modes and short asset ids are the ones
already published on Nostr (unchanged, so existing notes conform as
written). The spec tightens reading: a left boundary and Unicode-aware
boundaries for cashtags, no cashtags inside URLs or
nostr:entities, asset ids bound to the ticker, a fixed set of short-id shapes and chain slugs, and CAIP-19 accepted as the fourth element. Review: code spans skipped, combining marks and all Unicode numbers are word characters, mismatched resolved symbols are never shown as$TICKER(display vectors with a fixed resolver),tsearch tags derived from the content. Adds optionaltsearch tags and explains why NIP-73 isn't used yet.