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>API — coinexplorer</title>8<link rel="stylesheet" href="/assets/style.css">9</head>10<body>11<main>12 <div class="hero">13 <h1>coinexplorer API</h1>14 <p>Free, keyless, self-hosted market data for stablecoins and major crypto —15 indexed straight from public RPCs across 24 chains. Every endpoint below is16 live: hit <b>Try</b> to run it against this instance right now.17 OpenAPI/Swagger available at <a href="/docs">/docs</a>.</p>18 <div class="kpis">19 <div><b id="kChains">—</b><span>chains</span></div>20 <div><b id="kTransfers">—</b><span>transfers indexed</span></div>21 <div><b id="kTokens">—</b><span>assets tracked</span></div>22 <div><b id="kPrices">—</b><span>live prices</span></div>23 </div>24 </div>2526 <div class="docwrap">27 <nav class="toc" id="toc" aria-label="Endpoints"></nav>28 <div class="docbody" id="body"></div>29 </div>30</main>3132<script type="module">33import { $, j, fmtN, mountNav } from "/assets/app.js";34mountNav("API");3536const BASE = location.origin;3738const GROUPS = [39["Market data", [40 { m: "GET", p: "/v1/stablecoins/supply", d: "Latest on-chain circulating supply per token per chain (totalSupply / getTokenSupply / triggerconstantcontract), with USD totals. Snapshots hourly.",41 q: [["token", "filter one symbol, e.g. USDT", "no"]], t: "/v1/stablecoins/supply?token=USDT" },42 { m: "GET", p: "/v1/stablecoins/supply/series", d: "Supply snapshots over time per chain — powers the supply history charts.",43 q: [["token", "symbol (default USDT)", "no"], ["window", "15m / 24h / 7d style (default 7d)", "no"]], t: "/v1/stablecoins/supply/series?token=USDC&window=7d" },44 { m: "GET", p: "/v1/stablecoins/volume", d: "Rolling transfer volume in USD + transfer counts + distinct active senders/receivers, per chain. Served from precomputed aggregates (ms responses at any index size).",45 q: [["token", "symbol (default USDT)", "no"], ["window", "1h / 24h / 7d (default 24h)", "no"]], t: "/v1/stablecoins/volume?token=USDT&window=24h" },46 { m: "GET", p: "/v1/stablecoins/volume/series", d: "Time-bucketed volume per chain for line charts, precomputed for the standard windows.",47 q: [["token", "symbol", "no"], ["window", "1h / 24h / 7d", "no"], ["interval", "bucket size, e.g. 1h", "no"], ["chain", "restrict to one chain", "no"]], t: "/v1/stablecoins/volume/series?token=USDT&window=24h&interval=1h" },48 { m: "GET", p: "/v1/stablecoins/whales", d: "Largest transfers in the window, USD-valued with live prices (native BTC/ETH/SOL/TRX/BNB whales included). Backed by an incrementally-maintained whale table — floor $100K.",49 q: [["token", "symbol filter", "no"], ["window", "default 24h", "no"], ["min_usd", "threshold (default $1M)", "no"], ["limit", "max 500", "no"]], t: "/v1/stablecoins/whales?window=24h&min_usd=10000000&limit=5" },50 { m: "GET", p: "/v1/stablecoins/mints-burns", d: "Issuance events: transfers from the zero address are mints, to it are burns (EVM + Tron zero addresses).",51 q: [["token", "symbol filter", "no"], ["window", "default 24h", "no"], ["limit", "max 500", "no"]], t: "/v1/stablecoins/mints-burns?token=USDC&window=24h&limit=5" },52 { m: "GET", p: "/v1/stablecoins/flows", d: "Net issuance over time: minted minus burned per bucket — the flows chart.",53 q: [["token", "symbol filter", "no"], ["window", "default 7d", "no"], ["interval", "bucket size (default 1d)", "no"]], t: "/v1/stablecoins/flows?token=USDC&window=7d&interval=1d" },54 { m: "GET", p: "/v1/prices", d: "Latest USD prices used for valuation (CoinGecko keyless tier; stablecoins seeded at 1.0).",55 q: [], t: "/v1/prices" },56]],57["Explorer", [58 { m: "GET", p: "/v1/{chain}/transfers", d: "Recent indexed transfers on one chain, all tokens. Paginate with before_block.",59 q: [["symbol", "token filter", "no"], ["min_amount", "min USD", "no"], ["before_block", "pagination cursor", "no"], ["limit", "max 500", "no"]], t: "/v1/ethereum/transfers?limit=3" },60 { m: "GET", p: "/v1/{chain}/token/{id}/transfers", d: "Indexed transfers of one token contract/mint on one chain.",61 q: [["from / to", "address filters", "no"], ["min_amount", "min token units", "no"], ["since", "unix ts lower bound", "no"], ["before_block", "pagination", "no"]], t: "/v1/ethereum/token/0xdAC17F958D2ee523a2206206994597C13D831ec7/transfers?limit=3" },62 { m: "GET", p: "/v1/address/{addr}/transfers", d: "Cross-chain transfer history for any address (EVM hex, Tron T-addr, Solana pubkey, BTC bech32…).",63 q: [["limit", "max 500", "no"]], t: null },64 { m: "GET", p: "/v1/{chain}/tx/{hash}", d: "Transaction detail with decoded stablecoin/crypto transfers. EVM chains answer live from RPC; other families answer from the index.",65 q: [], t: null },66 { m: "GET", p: "/v1/{chain}/block/latest", d: "Live head block summary straight from the chain's free RPC pool (EVM).",67 q: [], t: "/v1/ethereum/block/latest" },68 { m: "GET", p: "/v1/{chain}/block/{n}", d: "Block summary + every stablecoin transfer inside it, decoded live (EVM).",69 q: [], t: null },70 { m: "GET", p: "/v1/{chain}/summary", d: "One-call chain profile: token registry, 24h volume by symbol, cursor/lag.",71 q: [], t: "/v1/bitcoin/summary" },72 { m: "GET", p: "/v1/search", d: "Universal resolver: classifies a tx hash / address / token symbol / chain name — checks the index first, so it can tell you which chain a tx lives on.",73 q: [["q", "the query string", "yes"]], t: "/v1/search?q=USDT" },74]],75["Live", [76 { m: "WS", p: "/v1/stream/transfers", d: "WebSocket pushing new transfers every ~2s as JSON arrays. Filters via query params.",77 q: [["token", "symbol filter", "no"], ["chain", "chain filter", "no"], ["min_usd", "value floor", "no"]], t: null,78 code: `const ws = new WebSocket("wss://${location.host}/v1/stream/transfers?min_usd=100000");79ws.onmessage = (e) => JSON.parse(e.data).forEach(t =>80 console.log(t.chain, t.symbol, t.value, "$" + t.usd, t.tx_hash));` },81]],82["Meta & infra", [83 { m: "GET", p: "/v1/tokens", d: "The verified asset registry: every token grouped by symbol with per-chain identifier, decimals, native vs bridged, category (stablecoin / crypto).",84 q: [], t: "/v1/tokens" },85 { m: "GET", p: "/v1/chains", d: "Chain registry: family, chain id, block time, confirmations, tokens.",86 q: [], t: "/v1/chains" },87 { m: "GET", p: "/v1/status", d: "Indexer cursors per chain: last block, live head, lag, backfill floor, row counts.",88 q: [], t: "/v1/status" },89 { m: "GET", p: "/v1/rpc/health", d: "Free-RPC endpoint health as scored by the indexer: success-rate EMA, latency, cooldowns.",90 q: [], t: "/v1/rpc/health" },91 { m: "GET", p: "/metrics", d: "Prometheus text exposition: transfers, per-chain lag, per-endpoint scores.",92 q: [], t: null },93]],94];9596const slug = (p) => p.replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "");97const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<");9899function card(ep) {100 const id = slug(ep.p);101 const params = ep.q.length ? `102 <div class="params"><table>103 <thead><tr><th>param</th><th>description</th><th>required</th></tr></thead>104 <tbody>${ep.q.map(([n, d, r]) =>105 `<tr><td class="mono" style="max-width:none">${n}</td><td style="white-space:normal">${d}</td><td>${r}</td></tr>`).join("")}106 </tbody></table></div>` : "";107 const curl = ep.m === "WS" ? "" :108 `<pre class="code"><span class="cm"># curl</span>109curl -s "${BASE}${ep.t || ep.p}"</pre>`;110 const code = ep.code ? `<pre class="code">${esc(ep.code)}</pre>` : "";111 const tryBtn = ep.t ? `112 <div class="tryrow">113 <button class="btn" data-try="${ep.t}" data-out="out-${id}">▶ Try it</button>114 <span class="sub" id="ms-${id}"></span>115 </div>116 <pre class="code try-out" id="out-${id}"></pre>` : "";117 return `<section class="ep" id="${id}">118 <div class="head"><span class="method ${ep.m === "WS" ? "ws" : ""}">${ep.m}</span>119 <span class="path">${ep.p}</span>120 <button class="btn copybtn" data-copy="${BASE}${ep.t || ep.p}">copy URL</button></div>121 <p class="desc">${ep.d}</p>122 ${params}${curl}${code}${tryBtn}123 </section>`;124}125126$("toc").innerHTML = GROUPS.map(([g, eps]) =>127 `<div class="group">${g}</div>` + eps.map((e) =>128 `<a href="#${slug(e.p)}">${e.p.replace("/v1/", "").replace("stablecoins/", "")}</a>`).join("")).join("");129$("body").innerHTML = GROUPS.map(([g, eps]) => eps.map(card).join("")).join("");130131document.querySelectorAll("[data-try]").forEach((b) => b.addEventListener("click", async () => {132 const out = $(b.dataset.out);133 out.style.display = "block";134 out.textContent = "…";135 const t0 = performance.now();136 try {137 const r = await fetch(b.dataset.try);138 const data = await r.json();139 const ms = Math.round(performance.now() - t0);140 document.getElementById("ms-" + b.dataset.out.slice(4)).textContent = `${r.status} · ${ms} ms`;141 let text = JSON.stringify(data, null, 2);142 if (text.length > 4000) text = text.slice(0, 4000) + "\n… (truncated)";143 out.textContent = text;144 } catch (e) { out.textContent = "request failed: " + e; }145}));146document.querySelectorAll("[data-copy]").forEach((b) => b.addEventListener("click", () => {147 navigator.clipboard?.writeText(b.dataset.copy);148 b.textContent = "copied ✓";149 setTimeout(() => (b.textContent = "copy URL"), 1200);150}));151152Promise.all([j("/v1/status"), j("/v1/tokens"), j("/v1/prices")]).then(([s, t, p]) => {153 $("kChains").textContent = Object.keys(s.cursors).length;154 $("kTransfers").textContent = fmtN(Object.values(s.indexed_transfers).reduce((a, b) => a + b, 0));155 $("kTokens").textContent = Object.keys(t).length;156 $("kPrices").textContent = Object.keys(p).length;157}).catch(() => {});158</script>159</body>160</html>161