Replace NonKYC with Nonlogs + cexswap.cc for WOW pricing

Per source repos in ~/src:
- ~/src/smirk-backend/src/infra/prices.rs uses Nonlogs api/markets,
  averaging (WOW-BTC * Kraken BTC/USD) and WOW-USDT routes.
- ~/src/neroswap-site/scrapers/scrape_cexswap.py hits
  cexswap.cc/api/public/markets/summary which already returns last_usd
  per pair.

WOW now sourced from BOTH and averaged — survives one source going down
without code changes.
This commit is contained in:
jwinterm
2026-05-14 09:20:09 -04:00
parent f4bf98f6fc
commit 5f76d77bd1
2 changed files with 99 additions and 32 deletions
+6 -3
View File
@@ -6,9 +6,12 @@ Why this exists: see the architectural notes in the wallet repo. Short version
## Sources ## Sources
* **Kraken** — BTC, XMR, LTC, DOGE, ETH, BCH, XNO (all → USD) * **Kraken** (`api.kraken.com/0/public/Ticker`) — BTC, XMR, LTC, DOGE, ETH, BCH, XNO (all → USD).
* **NonKYC** — WOW (WOW_USDT, treated as ≈ USD) * **Nonlogs.io** (`api.nonlogs.io/api/markets`) — WOW. Algorithm averages `WOW-BTC × Kraken BTC/USD` and `WOW-USDT` when both exist. Lifted from `~/src/smirk-backend/src/infra/prices.rs`.
* **Pinned** — USDT, USDC, DAI all return 1.00 (stables we don't quote upstream) * **cexswap.cc** (`cexswap.cc/api/public/markets/summary`) — WOW backup. Endpoint returns `last_usd` precomputed per pair, so we average across all WOW spot pairs.
* **Pinned** — USDT, USDC, DAI all return 1.00.
WOW USD price is the **average of whichever Nonlogs and cexswap routes returned a value** — survives one source going down without a code change.
## Endpoints ## Endpoints
+90 -26
View File
@@ -39,12 +39,11 @@ const KRAKEN_PAIRS: Record<string, string> = {
}; };
/** /**
* Wallet-side ticker → NonKYC market symbol. * Tickers we look up on Nonlogs and cexswap.cc.
* NonKYC quotes WOW only against USDT, which we treat as ≈ USD for spot pricing. * Wownero is the obvious one (delisted from major CEXes); add others here if
* they're not on Kraken either.
*/ */
const NONKYC_MARKETS: Record<string, string> = { const NICHE_TICKERS = ["WOW"] as const;
WOW: "WOW_USDT",
};
interface RatesPayload { interface RatesPayload {
rates: Record<string, number>; rates: Record<string, number>;
@@ -72,43 +71,108 @@ async function fetchKraken(): Promise<Record<string, number>> {
return out; return out;
} }
async function fetchNonkyc(): Promise<Record<string, number>> { /**
* Pull all markets from Nonlogs in one call. Returns per-ticker USD price
* by averaging the (TICKER-BTC × BTC/USD) and (TICKER-USDT) routes when both
* exist. Algorithm taken from ~/src/smirk-backend/src/infra/prices.rs.
*/
async function fetchNonlogs(btcUsd: number): Promise<Record<string, number>> {
const out: Record<string, number> = {}; const out: Record<string, number> = {};
for (const [ticker, market] of Object.entries(NONKYC_MARKETS)) { if (btcUsd <= 0) return out;
try {
const res = await fetch(`https://api.nonkyc.io/api/v2/ticker/${market}`, { const res = await fetch("https://api.nonlogs.io/api/markets", {
headers: { "user-agent": "hash-wallet-prices/0.1" },
cf: { cacheTtl: 30, cacheEverything: true }, cf: { cacheTtl: 30, cacheEverything: true },
}); });
if (!res.ok) continue; if (!res.ok) throw new Error(`nonlogs http ${res.status}`);
// NonKYC ticker shape: { "last_price": "0.0234", ... }. Field name has been const body = (await res.json()) as { markets?: Record<string, { last_price?: string }> };
// both `last_price` and `last` historically — accept either. const markets = body.markets ?? {};
const body = (await res.json()) as { last_price?: string; last?: string };
const last = body.last_price ?? body.last; for (const ticker of NICHE_TICKERS) {
if (last) out[ticker] = parseFloat(last); const sources: number[] = [];
} catch { const btcPair = markets[`${ticker}-BTC`]?.last_price;
// Skip — WOW just won't be in this refresh cycle. Stale KV value will be if (btcPair) {
// returned to clients until the next successful refresh. const p = parseFloat(btcPair);
if (p > 0) sources.push(p * btcUsd);
}
const usdtPair = markets[`${ticker}-USDT`]?.last_price;
if (usdtPair) {
const p = parseFloat(usdtPair);
if (p > 0) sources.push(p);
}
if (sources.length) {
out[ticker] = sources.reduce((a, b) => a + b, 0) / sources.length;
} }
} }
return out; return out;
} }
/**
* Pull all spot markets from cexswap.cc. Endpoint already returns `last_usd`
* per pair so no BTC conversion is needed. We average across all spot pairs
* for the same base ticker (e.g. WOW/USDT, WOW/BTC, WOW/XMR all converted to
* USD by cexswap upstream) for robustness.
*/
async function fetchCexswap(): Promise<Record<string, number>> {
const out: Record<string, number> = {};
const res = await fetch("https://cexswap.cc/api/public/markets/summary", {
headers: { accept: "application/json" },
cf: { cacheTtl: 30, cacheEverything: true },
});
if (!res.ok) throw new Error(`cexswap http ${res.status}`);
const body = (await res.json()) as
| { items?: Array<{ base?: string; last_usd?: number | string }> }
| Array<{ base?: string; last_usd?: number | string }>;
const items = Array.isArray(body) ? body : (body.items ?? []);
const buckets: Record<string, number[]> = {};
for (const m of items) {
const base = (m.base ?? "").toUpperCase();
if (!NICHE_TICKERS.includes(base as (typeof NICHE_TICKERS)[number])) continue;
const usd = typeof m.last_usd === "string" ? parseFloat(m.last_usd) : m.last_usd;
if (usd && usd > 0) {
(buckets[base] ??= []).push(usd);
}
}
for (const [ticker, prices] of Object.entries(buckets)) {
out[ticker] = prices.reduce((a, b) => a + b, 0) / prices.length;
}
return out;
}
async function refreshPrices(env: Env): Promise<RatesPayload> { async function refreshPrices(env: Env): Promise<RatesPayload> {
const [kraken, nonkyc] = await Promise.all([ // Kraken first — its BTC/USD is needed by the Nonlogs WOW conversion.
fetchKraken().catch((e) => { const kraken = await fetchKraken().catch((e) => {
console.error("kraken failed", e); console.error("kraken failed", e);
return {}; return {} as Record<string, number>;
});
const btcUsd = kraken.BTC ?? 0;
const [nonlogs, cexswap] = await Promise.all([
fetchNonlogs(btcUsd).catch((e) => {
console.error("nonlogs failed", e);
return {} as Record<string, number>;
}), }),
fetchNonkyc().catch((e) => { fetchCexswap().catch((e) => {
console.error("nonkyc failed", e); console.error("cexswap failed", e);
return {}; return {} as Record<string, number>;
}), }),
]); ]);
const rates: Record<string, number> = { ...kraken, ...nonkyc }; const rates: Record<string, number> = { ...kraken };
const sources: Record<string, string> = {}; const sources: Record<string, string> = {};
for (const k of Object.keys(kraken)) sources[k] = "kraken"; for (const k of Object.keys(kraken)) sources[k] = "kraken";
for (const k of Object.keys(nonkyc)) sources[k] = "nonkyc";
// For niche coins, average across whichever sources returned a price.
for (const ticker of NICHE_TICKERS) {
const samples: Array<[string, number]> = [];
if (nonlogs[ticker]) samples.push(["nonlogs", nonlogs[ticker]]);
if (cexswap[ticker]) samples.push(["cexswap", cexswap[ticker]]);
if (samples.length === 0) continue;
rates[ticker] = samples.reduce((a, [, p]) => a + p, 0) / samples.length;
sources[ticker] = samples.map(([s]) => s).join("+");
}
// Stables — we don't quote them upstream, just pin to 1.0. // Stables — we don't quote them upstream, just pin to 1.0.
for (const stable of ["USDT", "USDC", "DAI"]) { for (const stable of ["USDT", "USDC", "DAI"]) {