NKIPs

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.

NKIP5
TitleCashtags on Nostr
StatusDraft
TypeStandards Track
Created2026-10-27
LicenseCC0-1.0
RequiresNIP-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 $TICKER in 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:

  1. fenced code: from ``` to the next ```, or to the end of the text when unclosed;
  2. inline code: ` followed by one or more characters other than ` and newline, then `;
  3. URLs: http:// or https:// up to the next whitespace;
  4. 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:

  1. use only the first cashtag tag;
  2. 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;
  3. read a missing or unknown mode as compact;
  4. ignore an asset-id that 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:

  1. 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.
  2. 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 $TICKER anywhere: 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 i tags wouldn't know it;
  • an i tag can't carry the author's display choice (mode), which is half of what the cashtag tag is for;
  • i is single-letter and indexed, and clients already treat an i tag as "this note comments on that external thing", which a passing $XMR mention 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 t search 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 t tags, 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), t search tags derived from the content. Adds optional t search tags and explains why NIP-73 isn't used yet.

Markdown source · History · Comment