A desk that sizes a tokenised-equity position on has_tokens: true finds out at the ticket that the one Morgan Stanley wrapper on CoinMarketCap has no price, no volume, and no market CoinMarketCap tracks.
I built a small tool to count how often that happens. The first draft of the headline said the wrappers had never traded. That was wrong, and the way it was wrong is the most useful thing I learned in the build — so this post is about the retraction as much as the number.
- Live: shelfware.edycu.dev (the judge page is /judge)
- Repo: github.com/edycutjong/shelfware — MIT, stdlib-only Python
Two rows, two endpoint families, one word
CoinMarketCap's RWA API is new. /v5/real-world-assets/map lists every underlying it knows about — 7,811 of them — and flags 791 as has_tokens: true. /v5/real-world-assets/quotes/latest returns, per underlying, a tokens[] array of the wrappers minted against it, with the issuer that minted each one.
Here is Morgan Stanley on that surface, trimmed to the fields that matter:
{ "symbol": "MS", "name": "Morgan Stanley", "asset_type": "stock",
"rwa_rank": 43, "has_tokens": true,
"tokens": [ { "crypto_id": 41513, "symbol": "wMSx",
"issuer_name": "Backed Assets",
"price": null, "market_cap": null, "volume_24h": null } ] }
price: null. Nothing on the RWA surface says why. The reason lives in a different endpoint family, /v1/cryptocurrency/map, which is keyless and carries a status per listing:
{ "id": 41513, "symbol": "wMSx", "status": "untracked",
"platform": { "name": "X Layer",
"token_address": "0x2874A11805783324C54562eDB1A641C5d1d077a5" } }
CoinMarketCap's documentation defines untracked as "registered cryptocurrency projects that are listed but do not yet meet methodology requirements to have tracked markets." So: tokenised, yes; a market CoinMarketCap tracks, no. The boolean and the listing state disagree about what "tokenised" means, and nobody joins them — every price-based RWA tool drops the price: null rows before it starts.
Shelfware is built on exactly those rows.
The join, and the three counting rules
The engine is pure functions over the two ledgers. The rule that produces the headline:
def zero_tracked(wrappers, has_tokens_rows, strict=True):
"""rwa_ids of underlyings with no wrapper that has a CMC-tracked market.
strict (the headline): the underlying has >= 1 attached wrapper and EVERY one is untracked.
loose: no wrapper is active — also counts unresolved-only and inactive-only underlyings.
Strict is always <= loose; the headline never takes the larger number."""
groups = by_underlying(wrappers)
out = []
for r in has_tokens_rows:
ws = groups.get(r["rwa_id"], [])
if strict:
if ws and all(is_shelf(w) for w in ws):
out.append(r["rwa_id"])
elif not any(w.get("status") == "active" for w in ws):
out.append(r["rwa_id"])
return out
is_shelf is one line — status == "untracked" — and that line is the whole retraction. The first draft tested price is None. It gives the same number today, because on the committed census price == null ⇔ status == "untracked" held with 0 exceptions across 1,431 resolved wrappers. But it is the wrong predicate: a null price is an observation, untracked is CoinMarketCap's stated reason for it. The suite re-checks the equivalence on every run so the day CoinMarketCap prices an untracked wrapper, the join says so instead of quietly drifting.
Live on 2026-09-18, 5 keyed credits, 54 calls, 40.9 seconds:
-
476 of the 791 underlyings flagged
has_tokens: true— 60.2% — have no wrapper with a CMC-tracked market. Stocks alone: 437 of 689 (63%). - 673 of the 1,435 wrappers (46.9%) are
untracked. - By issuer: Dinari 27 attached, 0 tracked (100% shelf). Backed 772 attached, 140 tracked (82%), $669M live in the rest. Robinhood 8 of 106 on the shelf. Ondo 4 of 214. bStocks 0 of 77.
Re-derive the headline from the committed rows with no code at all:
jq '.wrappers | group_by(.rwa_id) | map(select(all(.[]; .status=="untracked"))) | length' data/census.json # 476
Why "never traded" had to go
The spike that settled it cost 2 credits and is committed as docs/proof/spike.json. For wMSx: map status untracked, quotes/latest price null, quotes/historical zero points — and, on the same RWA row, a tradfi_markets entry pointing at Binance's tokenised-stock venue for MS. Dinari's dShares trade on Dinari's own permissioned venue. A pool CoinMarketCap does not index is still a pool.
So "never traded" was a claim about the world that the data could not support. untracked is a claim about CoinMarketCap's coverage of what CoinMarketCap calls tokenised — which is exactly why it can be re-derived from CoinMarketCap's own rows. Every surface was re-worded to listed, no CMC-tracked market, and the readiness gate now fails the build if the stronger phrase comes back. The number did not change. What it means did, and the second meaning is the one an allocator can act on.
Answering a ticker with no key
The listing state is on the keyless /public-api surface, so the ticker question runs from a fresh clone with nothing installed:
$ git clone https://github.com/edycutjong/shelfware.git && cd shelfware
$ python3 -m shelfware MS
MS Morgan Stanley · stock · rwa_rank 43 · has_tokens: true
▒ wMSx Wrapped Morgan Stanley Tokenized Stock (xStock)
issuer Backed Assets
chain X Layer 0x2874A11805783324C54562eDB1A641C5d1d077a5
price null · market_cap null · volume_24h null
status UNTRACKED ← /public-api/v1/cryptocurrency/map, live, keyless, 0 credits
listed 2026-08-11 (date_added) · 38 days on the shelf
0 of 1 wrapper(s) with a CMC-tracked market
receipt: cmc/map symbol=wMSx → HTTP 200 · keyless · 0 credits to any key · 491 ms · sha256 d7e1ecdef293e6c8
The roster leg (tokens[]) is keyed, so without a key it comes from the committed snapshot and says so on the row; the state leg is live regardless. Every answer ends in a receipt line — endpoint, HTTP status, credits, body hash — because a number with no receipt is an opinion.
Numbers, briefly: the whole keyless question is p50 552 ms, p95 603 ms (n=9); the join over the 1,435 committed wrappers is 1.94 ms. 262 tests, 245 of them offline in about a second, engine coverage gated at 100%. A daily snapshot gives the shelf a time axis; day 5 recorded the first movement — MXL flipped active → untracked — and the page names it.
Where it breaks
-
untrackedis a listing state, not a claim about every venue. See above; it is the whole point. - The RWA family is keyed. Keyless callers get a dated roster and a live state.
- CoinMarketCap's map rejects its own dotted symbols (
NVDA.D,AI.FRx— 38 wrappers, HTTP 400 for the whole call). Those rows fall back to the coarser/v2/cryptocurrency/infovocabulary, and each row names which source answered. - 4 wrapper ids in
tokens[]resolve on no public CoinMarketCap surface. Shown in their own bucket, never counted as shelf. - The anonymous tier is per IP; a shared cloud egress can be refused outright. The CLI backs off, answers from the snapshot, and exits 75.
- It is a daily series, not a history. Untracked rows carry no dates;
date_addedis a listing day, not a market day.
Twelve dated findings for the CoinMarketCap API team — the plan gates, the two vocabularies for one coin's state, the symbol filter — are in FEEDBACK.md at the repo root, each with the evidence that produced it.
If you track tokenised assets, type a ticker at shelfware.edycu.dev and open the evidence drawer — it shows you the raw rows, so you don't have to take my word for the count either.
Top comments (0)