Files
labrinyang d73eaf873a fix(indexer): stop a manipulated pool from minting prices
A price used to propagate from any pool with enough depth on the priced side,
which let a thin pool with one inflated side mint a price for its other token
and carry it across the graph. Depth is now measured on the side that is
already credibly priced, and a pool-priced side may claim at most 100x that
depth.

Guard rails around it, all in one place: prices travel at most 3 hops from a
GT/anchor seed, |tick| beyond 700k is a broken init rather than a market,
stored prices must land in a plausibility band, and a TVL above $1B is treated
as corrupt instead of as a whale.

Adds a log tail so pool stats follow chain events instead of polling, a
demand-gated frontpage, and a last-good fallback so a failed refresh serves
the previous stat rather than a hole.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 23:59:56 +08:00

95 lines
3.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// GeckoTerminal enrichment — volume/liquidity/txn stats + token USD price
// seeds for the pricing waterfall. GT fully covers this chain's Uniswap
// deployments (network `robinhood`, per-dex top lists) but each list is capped
// at 10 pages × 20 = top 200 — the long tail keeps chain-derived TVL only.
// Free tier is 30 calls/min: calls are paced ≥ TUNE.gtPaceMs apart and the
// whole cycle (≤30 calls) runs every TUNE.statsMs.
//
// NOTE: GT has no UP33 dex entry — UP33 pool stats stay on the frontend's
// existing dexscreener path; this indexer only serves the Uniswap catalog.
import { GT, TUNE, log, sleep } from './config'
import { plausibleUsd } from './state'
import { poolRow, setTokenPrice, upsertStats } from './store'
const LISTS = [
{ path: '/networks/robinhood/pools', label: 'network' },
{ path: '/networks/robinhood/dexes/uniswap-v2-robinhood/pools', label: 'uni-v2' },
{ path: '/networks/robinhood/dexes/uniswap-v3-robinhood/pools', label: 'uni-v3' },
]
type GtPool = {
attributes?: {
address?: string
reserve_in_usd?: string
volume_usd?: { h24?: string }
transactions?: { h24?: { buys?: number; sells?: number } }
base_token_price_usd?: string
quote_token_price_usd?: string
}
relationships?: {
base_token?: { data?: { id?: string } }
quote_token?: { data?: { id?: string } }
}
}
let lastCall = 0
async function gtJson(url: string): Promise<{ data?: GtPool[] } | null> {
const wait = lastCall + TUNE.gtPaceMs - Date.now()
if (wait > 0) await sleep(wait)
lastCall = Date.now()
try {
const r = await fetch(url, { headers: { accept: 'application/json', 'user-agent': 'up33-lp-indexer/0.1' } })
if (!r.ok) return null
return (await r.json()) as { data?: GtPool[] }
} catch {
return null
}
}
const num = (x: unknown): number | null => {
const n = Number(x)
return Number.isFinite(n) ? n : null
}
const tokenOfId = (id?: string): string | null =>
id?.startsWith('robinhood_0x') ? id.slice('robinhood_'.length).toLowerCase() : null
function ingest(p: GtPool): boolean {
const a = p.attributes
const addr = a?.address?.toLowerCase()
if (!a || !addr || !poolRow(addr)) return false // catalog is the gate — unknown pools are ignored
const reserve = num(a.reserve_in_usd)
const h24 = a.transactions?.h24
const txns = h24 ? (h24.buys ?? 0) + (h24.sells ?? 0) : null
upsertStats(addr, num(a.volume_usd?.h24), txns, reserve, 'geckoterminal')
// Token price seeds — these are the CREDIBLE roots the whole pricing graph
// hangs off, and they're the one input the propagation rails can't second-
// guess, so an implausible GT quote is dropped rather than seeded.
const depth = (reserve ?? 0) / 2
if (depth > 0) {
const base = tokenOfId(p.relationships?.base_token?.data?.id)
const quote = tokenOfId(p.relationships?.quote_token?.data?.id)
const bp = num(a.base_token_price_usd)
const qp = num(a.quote_token_price_usd)
if (base && plausibleUsd(bp)) setTokenPrice(base, bp, depth, 'gt')
if (quote && plausibleUsd(qp)) setTokenPrice(quote, qp, depth, 'gt')
}
return true
}
/** one enrichment cycle over the three GT top lists */
export async function gtCycle(): Promise<void> {
let matched = 0
let seen = 0
for (const list of LISTS) {
for (let page = 1; page <= 10; page++) {
const j = await gtJson(`${GT}${list.path}?page=${page}`)
const items = j?.data
if (!items?.length) break
seen += items.length
for (const it of items) if (ingest(it)) matched++
if (items.length < 20) break
}
}
log(`[stats] gt cycle: ${matched}/${seen} list entries matched catalog`)
}