spb/coinexplorer Public MIT
Self-hosted, zero-API-key explorer for stablecoins and major crypto.
Python 60.3%
HTML 23.6%
JavaScript 8.1%
CSS 6.8%
SQL 1%
1<!doctype html>2<!-- Author: Simon-Pierre Boucher -->3<!-- Mail: contact@spboucher.ai -->4<html lang="en">5<head>6<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">7<title>coinexplorer — stablecoins & major crypto</title>8<link rel="stylesheet" href="/assets/style.css">9</head>10<body>11<main>12 <div class="pagehead"><h1>Market overview</h1>13 <span class="sub">on-chain data, indexed from free public RPCs</span></div>1415 <div class="filters">16 <select id="tokenSel" aria-label="Token"></select>17 <div class="presets" role="group" aria-label="Window">18 <button data-w="1h">1h</button><button data-w="24h" aria-pressed="true">24h</button><button data-w="7d">7d</button>19 </div>20 </div>2122 <section class="card tile"><h2>Tracked supply</h2>23 <div class="big" id="tSupply">—</div><div class="note" id="tSupplyN">all stablecoins, all chains</div></section>24 <section class="card tile"><h2>Volume <span class="sub" id="tVolW"></span></h2>25 <div class="big" id="tVol">—</div><div class="note" id="tVolN"></div></section>26 <section class="card tile"><h2>Transfers indexed</h2>27 <div class="big" id="tCount">—</div><div class="note">all tokens · all chains</div></section>28 <section class="card tile"><h2>Chains live</h2>29 <div class="big" id="tChains">—</div><div class="note" id="tChainsN"></div></section>3031 <section class="card twothird"><h2 id="volTitle">Volume over time</h2><div id="volChart"></div></section>32 <section class="card third"><h2>Assets by tracked supply (USD)</h2><div class="bars" id="supplyBars"></div></section>3334 <section class="card half"><h2 id="chainVolTitle">Volume by chain</h2><div class="bars" id="chainBars"></div></section>35 <section class="card half"><h2>Live transfers <span class="sub" id="wsState">connecting…</span></h2>36 <div class="scroll"><table>37 <thead><tr><th>chain</th><th>token</th><th class="num">amount</th><th>from → to</th><th>tx</th></tr></thead>38 <tbody id="feed"></tbody></table></div></section>3940 <section class="card"><h2>Largest transfers <span class="sub" id="whaleW"></span></h2>41 <div class="scroll"><table>42 <thead><tr><th>chain</th><th>token</th><th class="num">USD</th><th>from</th><th>to</th><th>tx</th><th>when</th></tr></thead>43 <tbody id="whales"></tbody></table></div></section>44</main>4546<script type="module">47import { $, j, fmtUsd, fmtN, fmtAmt, mountNav, hbars, lineChart, topSeries,48 addrLink, txLink, chainLink, tokenLink, ago } from "/assets/app.js";49mountNav("Overview");50let TOKEN = "USDT", WINDOW = "24h";51const ivl = () => ({ "1h": "5m", "24h": "1h", "7d": "6h" }[WINDOW]);5253async function refresh() {54 const [supplyAll, vol, status, series] = await Promise.all([55 j("/v1/stablecoins/supply"),56 j(`/v1/stablecoins/volume?token=${TOKEN}&window=${WINDOW}`),57 j("/v1/status"),58 j(`/v1/stablecoins/volume/series?token=${TOKEN}&window=${WINDOW}&interval=${ivl()}`),59 ]);60 const totalSupply = Object.values(supplyAll).reduce((a, s) => a + (s.total_usd ?? s.total), 0);61 $("tSupply").textContent = fmtUsd(totalSupply);62 $("tVol").textContent = fmtUsd(vol.total_volume);63 $("tVolW").textContent = "· " + WINDOW;64 $("tVolN").textContent = `${TOKEN} · ${fmtN(Object.values(vol.chains).reduce((a, c) => a + c.transfers, 0))} transfers`;65 $("tCount").textContent = fmtN(Object.values(status.indexed_transfers).reduce((a, b) => a + b, 0));66 $("tChains").textContent = Object.keys(status.cursors).length;67 const lagging = Object.values(status.cursors).filter((c) => c.lag > 100).length;68 $("tChainsN").textContent = lagging ? `${lagging} catching up` : "all at head";6970 $("volTitle").textContent = `${TOKEN} volume — ${WINDOW}`;71 lineChart($("volChart"), topSeries(series.chains, 3));7273 hbars($("supplyBars"), Object.entries(supplyAll)74 .map(([k, s]) => ({ k, v: s.total_usd ?? s.total, link: tokenLink(k) }))75 .sort((a, b) => b.v - a.v).slice(0, 11));7677 $("chainVolTitle").textContent = `${TOKEN} volume by chain — ${WINDOW}`;78 hbars($("chainBars"), Object.entries(vol.chains)79 .map(([k, c]) => ({ k, v: c.volume, link: chainLink(k),80 extra: `<br>${fmtN(c.transfers)} transfers` }))81 .sort((a, b) => b.v - a.v).slice(0, 12));82}8384async function refreshWhales() {85 const w = await j(`/v1/stablecoins/whales?window=${WINDOW}&limit=30`);86 $("whaleW").textContent = "· " + WINDOW;87 $("whales").innerHTML = w.map((x) =>88 `<tr><td>${chainLink(x.chain)}</td><td>${tokenLink(x.symbol)}</td>89 <td class="num"><b>${fmtUsd(x.usd)}</b></td><td>${addrLink(x.from)}</td>90 <td>${addrLink(x.to)}</td><td>${txLink(x.chain, x.tx_hash)}</td>91 <td>${ago(x.timestamp)} ago</td></tr>`).join("") ||92 `<tr><td colspan="7" class="empty">none in window</td></tr>`;93}9495function connectWS() {96 const ws = new WebSocket(`${location.protocol === "https:" ? "wss" : "ws"}://${location.host}/v1/stream/transfers?min_usd=500`);97 ws.onopen = () => $("wsState").textContent = "· live";98 ws.onclose = () => { $("wsState").textContent = "· reconnecting…"; setTimeout(connectWS, 3000); };99 ws.onmessage = (ev) => {100 const feed = $("feed");101 for (const r of JSON.parse(ev.data).slice(-25).reverse()) {102 const tr = document.createElement("tr");103 tr.innerHTML = `<td>${chainLink(r.chain)}</td>104 <td>${tokenLink(r.symbol)}</td><td class="num">${fmtAmt(r)}</td>105 <td>${addrLink(r.from)} → ${addrLink(r.to)}</td><td>${txLink(r.chain, r.tx_hash)}</td>`;106 feed.prepend(tr);107 }108 while (feed.rows.length > 60) feed.deleteRow(-1);109 };110}111112j("/v1/tokens").then((t) => {113 $("tokenSel").innerHTML = Object.keys(t).sort()114 .map((s) => `<option ${s === TOKEN ? "selected" : ""}>${s}</option>`).join("");115});116$("tokenSel").addEventListener("change", (e) => { TOKEN = e.target.value; refresh(); });117document.querySelectorAll(".presets button").forEach((b) => b.addEventListener("click", () => {118 document.querySelectorAll(".presets button").forEach((x) => x.setAttribute("aria-pressed", "false"));119 b.setAttribute("aria-pressed", "true"); WINDOW = b.dataset.w; refresh(); refreshWhales();120}));121refresh(); refreshWhales(); connectWS();122setInterval(refresh, 30_000); setInterval(refreshWhales, 60_000);123</script>124</body>125</html>126