Symbols are not identities
Every crypto data pipeline starts by keying on the ticker. Here is the measured cost of that decision, taken from a live registry.
A ticker feels like an identifier. It is short, it is unique on the exchange you are looking at, and every API returns one. So the first schema anybody writes looks like this.
-- The schema almost everyone writes first
CREATE TABLE price (
symbol text NOT NULL, -- 'BTC'
observed_at timestamptz NOT NULL,
price numeric NOT NULL,
PRIMARY KEY (symbol, observed_at)
);
-- It is not merely imprecise. It is wrong for 460 of
-- the 7,458 assets in our registry, and it will never
-- raise an error about any of them.The measurement
We counted, across our canonical asset registry, on 2026-08-31.
Roughly one asset in sixteen. Not a long-tail curiosity — assets that appeared in a ranked market snapshot, meaning they had real volume and real social activity at the time.
Three separate projects publish as HYPER: HyperChainX, Hyperlane and Hyperpigmentation. Three as NEIRO. Three as RAIN, SEND, VOLT, MAX, LIT, CAI, NAVI and BROCCOLI. The full list is browsable.
Why this asset class in particular
Equities have ISINs because a registrar assigns them. Crypto has no registrar. A ticker is chosen by whoever deploys the contract, and four structural forces push collisions upward:
- The namespace is tiny. Three to five uppercase characters, against tens of thousands of assets. Collisions are arithmetic.
- Cross-chain duplication is deliberate. The same name and ticker deployed on several chains, sometimes by the same team, sometimes not.
- Meme cycles reuse on purpose. A successful token spawns derivatives that adopt the ticker within days, because that is the point.
- Nothing prevents it. There is no authority to refuse the registration, because there is no registration.
Why the failure is invisible
This is the part that makes it dangerous rather than merely annoying. Keying on a symbol does not throw. There is no constraint violation, no parse error, no null. The pipeline runs, the row is inserted, the chart renders.
What you get is one series containing two projects' prices, spliced at whichever moments each was ranked. It looks like an asset with unusual volatility. Someone will eventually explain that volatility.
And a mis-keyed identity does not stay in one table. It propagates into every derived metric, every enriched view, every score, every backtest, and every report those produce. It is the highest-leverage bug in the entire domain and it is committed on the first day, in the schema.
What to key on instead
-- Identity is a thing you assign and hold
CREATE TABLE asset (
id bigint PRIMARY KEY,
symbol text NOT NULL, -- display only
name text NOT NULL -- display only
);
CREATE TABLE asset_provider_id (
asset_id bigint REFERENCES asset(id),
provider text NOT NULL,
provider_id text NOT NULL,
PRIMARY KEY (provider, provider_id) -- the real key
);
CREATE TABLE observation (
asset_id bigint REFERENCES asset(id),
observed_at timestamptz NOT NULL,
price numeric NOT NULL,
PRIMARY KEY (asset_id, observed_at)
);Three properties do the work:
asset.idis yours. You assign it, you keep it, and it survives every rebrand, ticker change and provider migration.- Provider identity is a separate relation. An asset has many provider identifiers; a provider identifier belongs to exactly one asset. That uniqueness constraint is the resolver.
- Symbol and name are display columns. Nothing joins on them. They can change, and changing them changes nothing else.
The resolution rules that follow
- Known provider id → existing asset. Deterministic.
- Unknown provider id → new asset. Do not guess by name.
- Cross-provider links are confirmed, never inferred. Record who confirmed them.
- Ambiguity is a value. Return candidates and let the caller decide.
Rule 2 is the one people push back on, because it produces duplicates. That is the correct trade. An accidental split is visible — two rows in a list — and fixable by adding a link. An accidental merge is invisible and produces a series that quietly contains two assets. One of those failures is recoverable.
What this looks like at the interface
A search endpoint returns a list, always, even when it finds one match. A consumer that wants "the" asset for a ticker has to confront the fact that there may not be one. That is not friction to be designed away; it is the domain.
It matters twice over for agents and language models, which will otherwise pick one candidate and narrate it confidently — discussed here.
The one-line version
A symbol is a label a project chose. An identity is something you assign and refuse to change. Confusing the two costs 6.2% of a registry, and nothing in the stack will tell you.
Related
- Entity resolution — how the layer implements this.
- Asset metadata — the canonical record.
- Normalizing market data — the stage after resolution.