One recipe, served on two chains.
RAGU inscribes NFTs on Zcash's transparent layer, mirrors the same collections to Solana via Metaplex Core, and serves both from a single catalog. The art lives on IPFS — the ownership lives on-chain.
Dashboard
syncing…Mint activity
last 14 daysBy chain
Recent inscriptions
live feed| Item | Collection | Chain | Owner | Age |
|---|
How it works
RAGU is a thin protocol over two very different chains. The trick is to keep everything that can be shared off-chain, and let each chain do ownership its own native way.
Pin the art
Collection metadata and assets go to IPFS once. Each token resolves to ipfs://<root>/<index>.json.
Inscribe on Zcash
A tiny OP_RETURN records the collection and token index. The token "lives" on a carrier UTXO; transferring it is just spending that output.
Mirror on Solana
A Metaplex Core asset is created with its uri pointed at the same IPFS file. One metadata record, two chains.
Serve from one catalog
A unified API normalizes Zcash indexer state and Solana's DAS into a single shape your app and marketplace read from.
Architecture
A shared off-chain layer feeds two chain adapters; both surface into one catalog the storefront reads from.
Zcash · Z721
Zcash's transparent layer behaves like Bitcoin, but has no native NFT primitive — so Z721 builds one. Data goes in an 80-byte OP_RETURN; ownership follows a carrier UTXO; an indexer enforces the rules.
// inscribe a mint: collection txid + token index → OP_RETURN payload const payload = z721.encodeMint({ collectionTxid, tokenIndex: 42 }); // 41 bytes — well under the 80-byte ceiling
Solana · Metaplex Core
On Solana there's almost no protocol to write: Metaplex Core handles ownership, transfers, and royalties, and DAS handles indexing. RAGU just creates an asset whose uri is the same IPFS file Zcash points at.
await create(umi, { asset, name: 'Genesis #42', uri: 'ipfs://<root>/42.json', // ← identical to the Z721 token collection, }).sendAndConfirm(umi);
Unified catalog
Each chain gets an adapter that normalizes to one token shape. The catalog hides the asymmetry — Zcash names/images are resolved from IPFS, Solana's come straight from DAS.
{ chain, collectionId, tokenId, owner,
name, image, uri, attributes, status }
A single owner query merges holdings across both wallets:
GET /catalog/owners?zcash=t1…&solana=So1…
→ { count: 2, tokens: [ {chain:'zcash'…}, {chain:'solana'…} ] }
API reference
| Method | Path | Returns |
|---|---|---|
| GET | /api/stats | Dashboard totals, activity, chain split, recent feed |
| GET | /catalog/collections | All collections across chains |
| GET | /catalog/tokens/:chain/:collection/:id | One normalized token |
| GET | /catalog/owners?zcash=&solana= | Merged holdings for a user |
| GET | /healthz | Health check (for Render) |
Deploy on Render
This dashboard is a plain Node web service. Push it to a Git repo and create a Render web service, or drop in the included render.yaml blueprint.
Nodenpm installnpm start/healthzprocess.env.PORT, which Render sets automatically — no config needed.