A reference implementation of a front-end dApp that reads tokenized-asset metadata from the Securitize public data feed and lets an investor:
- Browse the list of available tokenized assets
- Connect an EVM wallet (MetaMask or any EIP-1193 provider)
- Verify their wallet is whitelisted in the DS Protocol investor registry
- Buy an asset atomically by swapping USDC (or other stablecoins) via an on-ramp contract
- Sell an asset atomically via an off-ramp contract
- Redeem an asset by transferring tokens to the fund's redemption wallet
This project is intentionally minimal and self-contained so third-party developers can use it as a starting point for their own integrations.
Deep-dive reference:
docs/SECURITIZE_SKILL.md— a comprehensive technical guide to building dApps on top of Securitize tokenized assets: DS Protocol architecture, asset discovery, compliance checks, all relevant ABIs, chain metadata, known edge cases, and a step-by-step checklist. Read it before (or alongside) the source code.
npm install
npm run dev # http://localhost:3000/securitize-sampleBuild for production:
npm run build # emits static files in ./dist
npm run preview # serve ./dist locallyThe built dist/ folder is fully static — you can host it on any CDN or
static-file server (S3, Cloudflare Pages, Netlify, GitHub Pages, …). No
backend is required.
securitize-dapp-sample/
├── index.html # entry, loads ethers from CDN
├── package.json
├── vite.config.js # minimal Vite config, no middleware
├── LICENSE
├── NOTICE
├── README.md
├── docs/
│ └── SECURITIZE_SKILL.md # deep-dive developer guide
├── public/
│ ├── securitize-logo.svg
│ └── securitize-icon.svg
└── src/
├── main.jsx # router — mounts /securitize-sample
├── index.css
├── pages/SamplePage.jsx # the entire dApp in a single file
└── data/assetInfo.json # bundled cache of the public feed
The app sources all asset metadata (supported chains, token contracts, on-ramp/off-ramp contracts, redemption wallet addresses, icons, primary market URLs) from the Securitize public data feed at:
https://public-feed.securitize.io
At the time of writing, the public feed does not return
Access-Control-Allow-Origin headers, so a direct browser fetch from a
different origin will be blocked by the browser. The sample app handles
this gracefully:
- On load it attempts to fetch fresh data from the feed.
- If the fetch fails (CORS or any other reason) it logs a warning to the
console and falls back to the bundled
src/data/assetInfo.json.
This means the app always renders, even offline, using whatever data
was most recently committed to assetInfo.json.
It is the developer's responsibility to periodically refresh
src/data/assetInfo.json from the public feed and redeploy the app.
The app will never update that file by itself.
A trivial way to regenerate it from a terminal:
# List available symbols
curl -s https://public-feed.securitize.io/asset-info | jq .symbol
# Fetch per-asset data
curl -s 'https://public-feed.securitize.io/asset-info?symbol=BUIDL' | jq .A simple refresh script is easy to write — fetch the symbol list, fetch
each symbol's data, transform into the shape described below, and write
it to src/data/assetInfo.json. See the fetchLiveAssetInfo() function
in src/pages/SamplePage.jsx for the exact transformation the app uses
at runtime; running the same logic from Node is the simplest approach.
If you want live data without manually refreshing the JSON, run a small
backend that fetches the feed server-to-server (no CORS there) and
forwards the JSON to the browser with proper Access-Control-Allow-Origin
headers. Then point PUBLIC_FEED_BASE in SamplePage.jsx at your proxy.
This is out of scope for the sample app but only a few lines of Express /
Fastify / Cloudflare Worker.
Returns the list of symbols currently served by the feed.
{
"symbol": ["BUIDL", "ACRED", "VBILL", "HLSCOPE", "STAC", …]
}Returns per-blockchain deployment data for a single asset.
{
"data": [
{
"symbol": "BUIDL",
"name": "BlackRock USD Institutional Digital Liquidity Fund",
"icon": "https://…/buidl.png",
"decimals": "6",
"blockchain": "ethereum",
"chainId": 1,
"TokenAddress": "0x7712c34205737192402172409a8f7ccef8aa2aec",
"RedemptionWalletAddress": "0xbb543C77436645C8b95B64eEc39E3C0d48D4842b",
"onramp": [
{ "address": "0x…", "coin": "USDC" }
],
"offramp": [
{ "address": "0x…", "coin": "USDC" }
],
"opportunityURL": "https://id.securitize.io/primary-market/opportunities/123",
"aum": 12345678.9,
"nav": 1.0
},
{ /* …one entry per chain the asset is deployed on… */ }
],
"result": 12345678.9
}Field notes:
| Field | Meaning |
|---|---|
symbol |
Asset symbol (same as the query parameter) |
name |
Human-readable fund name |
icon |
PNG/SVG logo URL |
decimals |
ERC-20 decimals of the DS token (string, parse to int) |
blockchain |
Lowercase chain name (e.g. ethereum, polygon, avalanche, solana) |
chainId |
Numeric EVM chain id. Only present for EVM chains. Non-EVM entries (Solana, Aptos…) are filtered out by the app. |
TokenAddress |
Address of the DS token (ERC-20) on this chain |
RedemptionWalletAddress |
Fund wallet that receives tokens when an investor redeems off-chain (may be empty) |
onramp |
Array of { address, coin } entries for atomic buy contracts. Empty array when no on-ramp is deployed on this chain. |
offramp |
Array of { address, coin } entries for atomic sell contracts. Empty array when no off-ramp is deployed. |
opportunityURL |
Public primary-market page where a non-whitelisted investor can register |
aum, nav |
Assets under management (USD) and net asset value; handy for dashboards, not used by this sample |
result at the top level is the aggregate AUM of the asset across all chains.
An alternative endpoint that returns per-symbol, per-blockchain AUM and NAV aggregates. Not used by the sample dApp but useful if you want to render total AUM in your own UI.
GET https://public-feed.securitize.io/asset-stats
GET https://public-feed.securitize.io/asset-stats?symbol=BUIDL
The bundled src/data/assetInfo.json follows a normalised shape that the
sample app consumes directly:
{
"symbols": ["BUIDL", "ACRED", …],
"assets": {
"BUIDL": {
"symbol": "BUIDL",
"name": "BlackRock USD Institutional Digital Liquidity Fund",
"icon": "https://…",
"decimals": 6,
"purchaseUrl": "https://id.securitize.io/primary-market/opportunities/123",
"chains": [
{
"blockchain": "ethereum",
"chainId": 1,
"tokenAddress": "0x…",
"redemptionAddress": "0x…",
"onramp": [{ "address": "0x…", "coin": "USDC" }],
"offramp": [{ "address": "0x…", "coin": "USDC" }]
}
]
}
}
}The transformation from the raw feed response to this shape lives in
fetchLiveAssetInfo() in src/pages/SamplePage.jsx. Reuse it verbatim
from a Node refresh script if you want.
Before any UI surfaces a "Buy" button, the app calls the DS token's registry to check whether the connected wallet is a registered investor:
DSToken.getDSService(1) -> registry address
(falls back to registryService())
Registry.getInvestor(wallet) -> non-empty string iff whitelisted
If the wallet is not whitelisted, the app shows an overlay and
redirects to the asset's opportunityURL (primary-market page) so the
user can register.
1. Stablecoin.approve(onramp, amount)
2. onramp.calculateDsTokenAmount(amount) -> expected output
3. onramp.swap(amount, expectedOutput * 99%) -> 1% slippage tolerance
1. DSToken.approve(offramp, amount)
2. offramp.calculateLiquidityTokenAmount(amount) -> expected output
3. offramp.redeem(amount, expectedOutput * 99%)
Plain ERC-20 transfer of the DS token to the fund's
redemptionAddress. The fund processes redemption off-chain.
- More chains: add entries to the
CHAIN_METAmap inSamplePage.jsx. Anything not in that map is filtered out of the UI. - More stablecoins: add entries to the
KNOWN_STABLESmap for chains where the ramp contract doesn't exposeliquidity()/liquidityToken(). - More routes: the router lives in
src/main.jsx. The single/securitize-sampleroute is there to keep the sample focused.
- No backend, proxy, or server-side code (keep it inspectable)
- No wallet-connect libraries (uses raw EIP-1193
window.ethereum) - No transaction history / analytics / portfolio views
- No fiat on-ramp integrations
- No gas estimation UI beyond the basics ethers provides
These are easy to layer on top — start with this code as a base.
Licensed under the Apache License, Version 2.0. Fork it, modify it, ship it — commercial or not.
The Apache 2.0 license does not grant any right to use Securitize's
name, logo, or other brand assets (see Section 6 of the License and the
NOTICE file).
In plain English:
- You may reference "Securitize" factually to describe what your software does — e.g. "this dApp integrates with the Securitize public feed" — because that's accurate origin/interoperability language.
- You may not name your fork or product anything like "Securitize X", put the Securitize logo on your UI or marketing, or imply endorsement, sponsorship, or affiliation.
Trademark questions: legal@securitize.io.