SPB Git

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%
9.7 KB · 161 lines html
Raw Blame History
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, "&amp;").replace(/</g, "&lt;");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