24 KiB
Adding a coin to Morphit
This is the developer-facing playbook for adding a new cryptocurrency to Morphit. It covers two phases:
- What we need from the coin's community before any code is written.
- The file-by-file checklist for actually doing the work.
Sibling maintainer docs: ADDING-A-WORKSPACE.md
for adding apps/* or packages/*; LOCALE-GRADUATION.md
for graduating a PLANNED_LOCALES entry to SUPPORTED_LOCALES.
If you maintain or represent a cryptocurrency project and want yours added: read part 1, send us the requested data, and we take it from there. Reach the Agorise team via the contact URL in your local Morphit instance's footer.
If you're a Morphit developer about to add a coin: skim part 1 to understand what fixed inputs you have, then work through part 2.
Part 1 — What we need from the coin community
A request to add a coin should arrive with the following information. No surprises later, no "let me check on the icon" mid-implementation.
Required
-
Ticker symbol. The 3-5 character all-caps form (BTC, XMR, BLURT, ARRR). Must not collide with an already-listed coin or with a fiat currency code (we exclude USD, EUR, JPY, etc.).
-
Full name. How the coin is known in prose ("Bitcoin", "Monero", "Pirate Chain"). Used in pickers, tooltips, FAQ entries, and brag list. Pirate Chain is real as of cp41 (see ADR-0032), Decred is real as of cp43 (see ADR-0033), Solana is real as of cp45 (see ADR-0034), Ethereum is real as of cp47 (see ADR-0035), and Ripple is real as of cp49 (see ADR-0036); all five were hypothetical examples earlier. announcements.
-
One-line description. Maximum 120 characters. Plain prose, no slogans. This is what users see when picking a coin in the trade form. Examples already in the registry:
- "Privacy-focused cryptocurrency. Default and recommended on Morphit."
- "The original cryptocurrency. Recommend SegWit (bc1...) addresses."
-
Logo SVG. Two variants:
- Mono — single-color silhouette. We re-color this for dark-mode and themed rendering. Should look correct at 16×16 px.
- Color — full brand-color version. Used in marketing pages and orderbook list items.
Both must be vector, valid SVG 1.1, and free of embedded raster images. No
<text>elements (font fallback issues); convert text-in-logo to outlined paths. Square viewBox. Origin (0, 0) top-left. -
Address format specification. Enough detail to construct a regex-based shape check:
- First-character constraint (e.g., starts with "1", "3", or "bc1" for BTC; "4" or "8" for XMR; "zc" for ZEC shielded; etc.).
- Length range (BTC: 26–62 chars; XMR: 95 or 106).
- Allowed character set (Base58, Base32-bech, hex, etc.).
We do not perform checksum verification at the address- shape layer (that would require bundling per-coin libraries and is the recipient wallet's job). Cheap shape check only.
-
TxID format. How the coin's confirmed-transaction identifier looks.
- Length (BTC: 64 hex; XMR: 64 hex; etc.).
- Character set.
- Whether confirmations are needed before display (and how many is "safe enough" for our verifier).
-
Smallest-unit decimals. How many decimal places a transfer amount carries on chain. BTC: 8 (satoshi). XMR: 12 (piconero). BLURT: 3. Used for amount-display rounding and form input precision.
Conditional
-
Memo / payment-id support. Whether the coin's base transaction can carry an arbitrary memo or payment-id field that's visible to both sender and recipient.
- Yes (BLURT, XLM): we expose a memo input in the address- share modal.
- No (BTC, modern XMR): we omit it.
- "Yes but discouraged" (legacy XMR payment-IDs): we omit it; subaddresses are the path forward.
-
Public block-explorer JSON API endpoint. If the coin has a public, maintained, no-API-key explorer that returns transaction details (sender, recipient, amount, txid, confirmations) as JSON, we can wire automatic verification for fee-payments in this asset. Without this, fee-payments in this coin require manual operator confirmation.
What we need:
- URL template:
https://explorer.example/api/tx/{TXID} - Documented response schema (what field carries the amount, recipient address, etc.).
- Rate limits.
- Funding model — is the explorer maintained by the coin's foundation or by a third party? Affects how comfortable we are depending on it.
- URL template:
-
Color theme accent. Hex code or Tailwind palette name matching the coin's brand identity. We use this for border accents, dot indicators, and the chip background. Example values: BTC
text-amber-500, XMRtext-orange-500, BLURTtext-morphit-emerald.
Out of scope (don't send these)
- Marketing copy. We write our own.
- Banner ads or sponsorship offers. Not what we do.
- Fiat conversion rates. We don't handle those; users agree on prices peer-to-peer.
- Wallet recommendations. We may suggest wallets in support docs, but that's case-by-case.
Part 2 — File-by-file checklist
This section assumes you have all the data above on hand. Time estimate for an experienced Morphit developer: half a day of code, plus a translation pass for the user-facing strings (another day if any new strings need translation across all 10 locales).
Step 1 — Bundle the logo
apps/web/static/coins/<lower-ticker>.svg
Mono variant. Viewbox 0 0 24 24 or similar square.
Test: open the SVG in a browser at multiple sizes (16, 24, 48, 96 px) and confirm it stays crisp and recognizable.
Step 2 — Extend the chain-payload schema
If the new coin will be used as a payment method in chat (address-share, funds-sent) or as a listing-fee currency, extend the on-chain payload schema:
File: apps/web/src/lib/chat/payload.ts
Add to the PaymentMethod type union:
export type PaymentMethod = 'btc' | 'xmr' | 'blurt' | '<new>';
IMPORTANT — PROTOCOL VERSION:
Existing indexers will REJECT any chain op carrying an unknown
method field as "schema violation". This means adding a new
method requires either:
- (a) Bumping the op version (currently
v: 1for chat payloads,v: 1for orders) so old indexers see it as a newer-version op they ignore (forward-compat fallthrough); OR - (b) Coordinating a federation-wide indexer rollout before any frontend can broadcast the new method.
Option (a) is preferred when possible — it preserves backward compatibility. Option (b) is acceptable for tightly- coordinated rollouts.
File: apps/web/src/lib/orders/payload.ts
Add to the fee_method union:
readonly fee_method?: 'blurt' | 'waived_first_buy' | 'btc' | 'xmr' | '<new>';
Same protocol-version considerations apply.
Step 3 — Frontend asset registry
File: apps/web/src/lib/assets/registry.ts
Add a new entry to ASSETS:
{
ticker: '<new>',
displayTicker: '<NEW>',
displayName: '<Full Name>',
oneLineDescription: '...',
logoSvgPath: '/coins/<new>.svg',
accentClass: 'text-<color>-<shade>',
decimals: <integer>,
supportsMemo: <bool>,
addressValidator: validators.<new>,
canBeUsedForListingFee: <bool>,
canBeTraded: <bool>
}
Add the validator regex above the validators object. Keep
it permissive (cheap shape check); see existing entries for
style.
Run the smoke:
tsx apps/indexer/scripts/asset-registry-smoke.ts
Then add new scenario(s) for the new coin's address validator (positive and negative cases).
Step 4 — Indexer chain-payload validation
File: apps/indexer/src/indexer/handlers/order.ts
Find the asset: and fee_method: zod schemas. Add the new
ticker. Same for any per-method conditional checks (e.g.
"externalTxId is required when method is btc/xmr/").
File: apps/indexer/src/indexer/handlers/chat.ts (if
chat-payload methods extended)
Same — add the new method to validation.
Step 5 — Database schema (if explorer-verifier wired)
If the new coin gets explorer verification (per Part 1, item 9):
File: apps/indexer/src/db/schema.sql
Append a new migration section at the bottom of the file in the same style as the existing v29/v30/v31 blocks:
-- ─── Migration vN — <new coin> explorer-verified payments ───────
Pre-Phase-3 the convention was a separate schema-v<N>.sql
file in apps/indexer/src/db/; that approach was collapsed
in May 2026 (see MIGRATIONS in
apps/indexer/src/db/migrations.ts — historical per-version
files are now archived under
apps/indexer/src/db/historical/). All new schema changes
go inline in schema.sql and register as additive migration
entries in MIGRATIONS. See docs/adr/0001-... for the
migration contract.
Pattern for the verifier code: see how
bitcoinExplorerVerifier.ts and moneroProofVerifier.ts
write their state. (Monero uses per-payment tx_proof
verification rather than view-key-based explorer scraping
since Part 108++; the BTC verifier remains the canonical
explorer-style template.)
File: apps/indexer/src/indexer/fee/<new>ExplorerVerifier.ts
New verifier module. Use the existing BTC and XMR verifiers as
templates. Implements the Verifier interface defined in
fee/verifier.ts.
Step 6 — Order form + chat modals
File: apps/web/src/lib/components/AddressShareModal.svelte
Find the per-method branches (current code switches on
method === 'btc' / 'xmr' / 'blurt'). Replace with calls
through getAsset(method) from the registry — picks up the
new entry automatically once the registry is updated.
File: apps/web/src/lib/components/FundsSentModal.svelte
Same pattern — switch hard-coded branches to registry lookups where reasonable.
File: apps/web/src/routes/[lang]/post/+page.svelte
The order-creation form. Find the asset-picker UI. Replace
hardcoded <button>BTC</button> <button>XMR</button> triples
with {#each tradeableAssets() as a} loops over the registry.
Step 7 — i18n (NEW STRINGS)
If any new user-visible strings are needed (e.g., a coin- specific hint or warning), add them across all 10 locales:
apps/web/src/lib/i18n/locales/{en,es,fr,de,it,pl,ru,fa,zh-CN,zh-HK}.json
Run the parity check:
python3 -c "
import json, glob
def flat(d, p=''): yield from (yield_recurse(d, p))
# (see existing locale-parity check in scripts/run-smokes.sh)
"
Make sure no locale lags behind English.
Step 8 — Tests
For every change above, add or update smoke scenarios:
apps/indexer/scripts/asset-registry-smoke.ts— registry shape and validators.apps/indexer/scripts/order-handler-smoke.ts(if exists) — the new asset accepted by the indexer.apps/indexer/scripts/listener-dispatch-smoke.ts— if the new coin has chat-payload paths.- Visual: open the
/postform, the orderbook, and the chat address-share modal in dev and confirm the new coin appears in pickers and renders its accent color correctly.
Step 9 — Documentation update
- Add a brief mention of the new coin in
docs/ARCHITECTURE.md(the "What runs where" section). - Update
docs/OPERATIONS.mdif operators need to do anything per-coin (e.g., set up an explorer API key). - Bump the changelog entry.
Step 10 — Coordination
- Open a PR with all of the above.
- Ping the requesting coin's contact for review of the description, logo rendering, and address-validation behavior.
- Get review from at least one other Morphit core developer.
- Stage on a non-canonical instance for at least 72h before enabling on the canonical morphit.io.
What we will NOT do
- Add a coin without all the Part 1 inputs.
- Add a coin whose address format is unstable or under active consensus debate (we'd have to keep updating the validator).
- Add a coin that requires us to bundle and ship a per-coin library larger than 50 KB (we keep the bundle small).
- Add a coin whose primary use is a centralized exchange's internal token.
Future infrastructure: Matrix bridge bot
Coin communities reach us via Matrix today. We're planning a Matrix↔Morphit-chat bridge bot so coin-community discussions about Morphit features can flow naturally between protocols. Out of scope for the current iteration; tracked separately.
2026-05-13 architectural update (Part 121) — trade-only assets + multi-network coins
Memory #23 established a hard architectural invariant that clarifies what kinds of assets can be added in each role:
Listing fees can ONLY be paid in BLURT, XMR, or BTC. This is
not a configuration knob; it's a wire-format-frozen decision.
The indexer's fee_method enum at
apps/indexer/src/indexer/handlers/order.ts is exactly the
4-member set 'blurt' | 'waived_first_buy' | 'btc' | 'xmr'. Two
sentinel-grep smokes enforce this (see "Smoke coverage" below).
This split breaks coin additions into two categories:
Category A — full-citizen coin (rare, requires deep operator
trust)
Used both as a tradable asset AND as a fee-payment method. Adding one is a HARD breaking change because the wire-format fee_method enum expands. Every operator must agree to verify this coin's payments before federation can continue without divergence. In practice we expect this category to be closed at BLURT/XMR/BTC and not reopen. If a future case arises, treat it as a charter-level decision, not a routine PR.
Category B — trade-only coin (the common case for new
additions)
Used only for peer-to-peer trading between users. Cannot be used for listing fees, cold-message fees, or featured-slot bids. Adding one is a much smaller change:
- Single entry in
packages/asset-registry/src/index.tswithcanPayListingFee: falseANDcanBeTraded: true. The asset-registry-smoke validates the invariant thatcanPayListingFee: true → ticker ∈ {BLURT, BTC, XMR}, so a miswired entry fails CI loudly. - Mirror in
apps/web/src/lib/assets/registry.tswithcanBeUsedForListingFee: false. - No fee-verifier needed (the asset can't pay fees).
- Standard logo + i18n + address validator + ADDING-A-COIN Part 1 inputs.
The two new sentinel-grep smokes guarantee a Category B coin cannot accidentally leak into the fee path:
packages/asset-registry/scripts/fee-method-enum-frozen-smoke.ts— asserts the indexer'sfee_methodfield type union stays exactly the 4-member frozen set. Belt + suspenders against someone adding'usdt'to the enum out of habit.packages/asset-registry/scripts/first-buy-waiver-payment-agnostic-smoke.ts— asserts the first-buy waiver gate fires on (side=buy, asset=BLURT) regardless ofpayment_methods, so a new user's first BLURT buy still gets the waiver even if they pay their counterparty in USDT.
Multi-network coins (USDT, USDC, DAI; possible future ETH, ARRR, etc.)
A new asset-registry field supportedNetworks: readonly string[]
declares which networks an asset exists on. Single-network
coins (BTC, XMR, BLURT, BCH, LTC, DASH, DOGE, ZEC, ARRR, DCR, SOL, ETH, XRP) declare ['mainnet'].
Multi-network coins list each network explicitly. As of Part
122 cp31 three multi-network assets are shipped:
- USDT (Part 121 cp3) —
supportedNetworks: ['erc20', 'trc20', 'spl', 'bep20']. Each network has a visually distinct address format (TRC-20 starts withT, ERC-20/BEP-20 start with0x, SPL is base58 32-44 chars), so users can usually tell at a glance which network an address belongs to. - USDC (Part 122 cp30) —
supportedNetworks: ['erc20', 'spl', 'base', 'polygon']. WATCH OUT: three of the four (ERC-20, Base, Polygon) all use the EVM0x[40 hex]address format — they are visually IDENTICAL. An address that's valid on Polygon is also a valid address on Ethereum AND on Base, but each chain holds its own separate USDC balance for that address. The network discriminator field is the ONLY thing telling the sender's wallet which chain to broadcast on. Cross-network-mis-send is more dangerous on USDC than on USDT for this reason; the per-message cross-network warning inChatMessage.sveltereflects this. See ADR-0028 §"Why surface the EVM identical-address warning so prominently?". - DAI (Part 122 cp31) —
supportedNetworks: ['erc20', 'polygon', 'base', 'arbitrum']. EVEN MORE DANGEROUS than USDC for cross-network confusion: ALL FOUR DAI networks share the EVM0x[40 hex]format (no Solana SPL variant for DAI — SPL/TRC-20/BEP-20 versions of DAI are bridged/ wrapped, not Maker-native, and Morphit intentionally excludes them per ADR-0029 §1). DAI is the highest cross-network address-confusion surface on Morphit; the network picker carries the strongest warning. Unlike USDT/USDC, DAI's token contract has no admin freeze function (MakerDAO can't blacklist), but is partly USDC-backed via the Peg Stability Module — distinctdai_partly_centralizedprivacy-warning class (vsusdt_centralized/usdc_centralized).
The canonical reference is the actual USDT, USDC, and DAI
entries at packages/asset-registry/src/index.ts. USDT:
{
ticker: 'USDT',
decimals: 6,
isCoordinationChain: false,
canBeTraded: true,
canPayListingFee: false, // Category B
supportedNetworks: ['erc20', 'trc20', 'spl', 'bep20'],
defaultNetwork: null, // force explicit user choice
privacyWarningKey: 'usdt_centralized',
addressShape:
/^(0x[a-fA-F0-9]{40}|T[1-9A-HJ-NP-Za-km-z]{33}|[1-9A-HJ-NP-Za-km-z]{32,44})$/,
privacyFeatures: { // Part 122 cp26 — required
freshAddressAdvice: 'hd-derived',
optInPrivacyTech: null, // USDT has no chain-level opt-in
privacyGuideKey: 'usdt'
}
}
USDC mirrors the structure (note the simpler regex — no Tron T-prefix branch because Circle doesn't issue natively on Tron):
{
ticker: 'USDC',
decimals: 6,
isCoordinationChain: false,
canBeTraded: true,
canPayListingFee: false, // Category B (same as USDT)
supportedNetworks: ['erc20', 'spl', 'base', 'polygon'],
defaultNetwork: null, // force explicit user choice
privacyWarningKey: 'usdc_centralized',
addressShape:
/^(0x[a-fA-F0-9]{40}|[1-9A-HJ-NP-Za-km-z]{32,44})$/,
privacyFeatures: {
freshAddressAdvice: 'hd-derived',
optInPrivacyTech: null, // USDC has no chain-level opt-in
privacyGuideKey: 'usdc'
}
}
Setting defaultNetwork: null forces the post-order form to
require an explicit network pick on every trade — the safest
stance for cross-chain-mis-send-prone assets. Per-network
metadata (regexes, fee hints, bundled explorer URLs) lives
separately in apps/web/src/lib/assets/networks.ts; adding a
new network to an existing multi-network asset is a single
entry there.
Indexer wire-format wiring — both the indexer's InstanceResponse
interface AND the indexer-client mirror declare a per-network
chat_link_urls.<asset> sub-map (Part 122 cp30-DD-10/11 — when
adding a new multi-network asset, do NOT just add the field to
the indexer-client mirror. The indexer-side body construction
must ALSO populate it from the corresponding Config field + Zod
env-var schema entry). The 4 canonical wire-format surfaces
that must ALL be updated for any new multi-network asset:
- Frontend store interface + defensive
?? {…}fallback + fetch normalization (apps/web/src/lib/stores/instance.ts) - Indexer-side InstanceResponse interface + body construction
(
apps/indexer/src/api/instance.ts) - Indexer-client mirror (
packages/indexer-client/src/index.ts) - Matrix-bot api-response-shape-smoke ChatLinkUrlsSchema
(
apps/matrix-bot/scripts/api-response-shape-smoke.ts)
Plus the Config schema fields + env-var Zod entries in
apps/indexer/src/config/index.ts, the example env file at
ops/env/indexer.env.example, and the ops-cli wizard's
ChatLinkExplorersResult + step prompts + render emission.
The frontend address-share modal validates the address against
the chosen network's regex (not just the registry's combined
regex), and the <ChatMessage> component renders a bold-network
prefix + amber-warning (USDT) or blue-warning (USDC) aside
above the address: "Tron (TRC-20) USDT address — send USDT on
Tron only. Sending USDT on any other network to this address
loses your funds permanently."
Full architectural rationale: docs/adr/0023-usdt-multi-network.md,
docs/adr/0028-usdc-multi-network-trade-only-addition.md, and
docs/adr/0029-dai-multi-network-trade-only-addition.md.
Privacy warning chip
A new field privacyWarningKey: string | null opts an asset
into rendering a localized privacy/decentralization warning in
the post-order form and address-share modal. null means no
warning (BTC, XMR, BLURT, BCH, LTC, DASH, DOGE, ZEC, ARRR, DCR,
SOL, ETH, XRP all have null — they're either private,
decentralized, or transparent-but-non-custodial enough that no
warning is needed). Non-null is an i18n key looked up under
assets.privacy_warnings.<key> in the locale JSON.
The three stablecoins ship a warning:
- USDT's warning (Part 121 cp3,
assets.privacy_warnings.usdt_centralized) - USDC's warning (Part 122 cp30,
assets.privacy_warnings.usdc_centralized) - DAI's warning (Part 122 cp31,
assets.privacy_warnings.dai_partly_decentralized)
USDT/USDC explain:
- The issuer (Tether Inc. for USDT, Circle for USDC) can freeze any address on any supported network.
- Transactions are public on whichever network the user chose (no on-chain privacy).
- Morphit can't make centralized stablecoins private — only XMR and ARRR have meaningful on-chain privacy by construction.
DAI explains the partial-decentralization nuance: the Maker contract has no admin-controlled freeze function, but Dai is partly backed by USDC via the Peg Stability Module — so Circle's freeze power transitively affects DAI's redemption mechanics (documented in ADR-0029 §3).
This warning is required by Memory #19 (privacy is priority #1): users must be told when an asset they're considering fails the privacy bar.
Privacy framework (privacyFeatures struct)
Every AssetEntry carries a privacyFeatures struct (shipped in
Part 122 cp26 — see docs/adr/0026-transparent-chain-privacy-framework.md).
The struct drives four user-facing surfaces simultaneously:
amount-jitter, address-reuse warnings, opt-in privacy-tech
listings, and the per-asset privacy guide page at
/[lang]/privacy/{key}.
Three fields, all required:
freshAddressAdvice— one of'subaddress'(XMR-style),'hd-derived'(BTC and forks, transparent UTXO chains), or'account-reuse'(account-model chains like BLURT where the address IS the account name).optInPrivacyTech—nullif the chain has no in-protocol opt-in privacy tech, OR an array of protocol-standard identifiers from the fixed enum:'mweb' | 'cashfusion' | 'coinjoin' | 'payjoin' | 'privatesend'. These are PROTOCOL NAMES not wallet names — Morphit never endorses specific wallets.privacyGuideKey— lowercase i18n key prefix. Pages live at/[lang]/privacy/{key}and pull fromprivacy.guides.{key}.*i18n strings.
If a new coin has a privacy tech not in the enum, extend the
enum in packages/asset-registry/src/index.ts AND in
docs/adr/0026-transparent-chain-privacy-framework.md's table.
Then add localized copy under privacy.opt_in_tech.{tech}.{name,explain}
× 10 locales. Same path DASH took to add 'privatesend' (cp27,
ADR-0027 §7).
Required i18n keys per new asset (× 10 locales):
privacy.guides.{key}.one_line— under-80-char summary shown on the/privacyindex page next to the asset icon.privacy.guides.{key}.intro— 2-3 sentence intro paragraph shown on the per-asset guide page.privacy.guides.{key}.meta_description—<meta name=description>for SEO + social cards.privacy.guides.{key}.caveats— honest disclosure of where the asset's privacy story falls short and what stronger alternatives exist on Morphit (typically XMR).
Skip the caveats key if the asset has no caveats worth flagging (rare — every asset has some compromise).