```
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
```

# NKIP-0005: Cashtags on Nostr

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.

> **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](https://github.com/nostr-protocol/nips/pull/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.
