|
2 | 2 | // |
3 | 3 | // The live USD price of FAIR is sourced from WFAIR — the wrapped-FAIR bridge |
4 | 4 | // token on Base L2 (1:1 peg). Public price responses deliberately rely on |
5 | | -// indexed market data instead of the current Uniswap V3 pool slot0 spot price: |
6 | | -// the pool can be low-liquidity or single-sided, so its instantaneous tick is |
7 | | -// not a safe oracle for the official explorer price. |
| 5 | +// GeckoTerminal's indexed price for the WFAIR/USDC pool instead of reading the |
| 6 | +// pool's current Uniswap V3 slot0 spot price on-chain: an instantaneous tick on |
| 7 | +// a low-liquidity pool is manipulable within a block and is not a safe oracle. |
| 8 | +// (GeckoTerminal's token-level price for WFAIR is null, so we read the pool, not |
| 9 | +// the token, endpoint.) |
8 | 10 | // |
9 | 11 | // This module is the single source of truth for the price computation. Both the |
10 | 12 | // REST route (`routes/price.ts`) and the MCP server consume `getPrice()` here so |
|
13 | 15 | /** WFAIR on Base, 18 decimals. */ |
14 | 16 | const WFAIR_ADDRESS = "0xF2853CedDF47A05Fee0B4b24DFf2925d59737fb3" as const; |
15 | 17 |
|
| 18 | +/** WFAIR/USDC Uniswap V3 pool on Base — the pool GeckoTerminal indexes a price for. */ |
| 19 | +const WFAIR_USDC_POOL_ADDRESS = "0x9F4F694390c60b51e30461c785C1345A1545b7ca" as const; |
| 20 | + |
16 | 21 | const WFAIR_NETWORK = "base"; |
17 | 22 | export const PRICE_SOURCE_INDEXER = "wfair-base" as const; |
18 | 23 | export type PriceSource = typeof PRICE_SOURCE_INDEXER; |
@@ -53,14 +58,18 @@ const EMPTY_CONTEXT: PriceContext = { |
53 | 58 |
|
54 | 59 | // ---- Upstream response shapes (only the fields we consume) ---- |
55 | 60 |
|
56 | | -interface GeckoTerminalTokenResponse { |
| 61 | +interface GeckoTerminalPoolResponse { |
57 | 62 | data?: { |
58 | 63 | attributes?: { |
59 | | - price_usd?: string | null; |
60 | | - volume_usd?: { h24?: string | null } | null; |
| 64 | + base_token_price_usd?: string | null; |
| 65 | + quote_token_price_usd?: string | null; |
| 66 | + reserve_in_usd?: string | null; |
61 | 67 | market_cap_usd?: string | null; |
62 | | - fdv_usd?: string | null; |
63 | | - total_reserve_in_usd?: string | null; |
| 68 | + volume_usd?: { h24?: string | null } | null; |
| 69 | + price_change_percentage?: { h24?: string | null } | null; |
| 70 | + } | null; |
| 71 | + relationships?: { |
| 72 | + base_token?: { data?: { id?: string | null } | null } | null; |
64 | 73 | } | null; |
65 | 74 | } | null; |
66 | 75 | } |
@@ -105,21 +114,26 @@ async function fetchJson<T>(url: string): Promise<T> { |
105 | 114 | } |
106 | 115 |
|
107 | 116 | /** |
108 | | - * Preferred public price and context from GeckoTerminal: price, 24h volume, |
109 | | - * on-chain reserve value (surfaced as liquidity), and market cap. |
| 117 | + * Preferred public price and context from GeckoTerminal's WFAIR/USDC pool: |
| 118 | + * the indexed token price, 24h volume and change, pool reserve value (surfaced |
| 119 | + * as liquidity), and market cap. WFAIR is the pool's base token, but we resolve |
| 120 | + * base-vs-quote from the response so a reordered pool still reads the right side. |
110 | 121 | */ |
111 | 122 | async function loadFromGeckoTerminal(): Promise<{ price: number | null; context: PriceContext }> { |
112 | | - const body = await fetchJson<GeckoTerminalTokenResponse>( |
113 | | - `${GECKOTERMINAL_BASE}/networks/${WFAIR_NETWORK}/tokens/${WFAIR_ADDRESS}`, |
| 123 | + const body = await fetchJson<GeckoTerminalPoolResponse>( |
| 124 | + `${GECKOTERMINAL_BASE}/networks/${WFAIR_NETWORK}/pools/${WFAIR_USDC_POOL_ADDRESS}`, |
114 | 125 | ); |
115 | 126 | const attrs = body.data?.attributes ?? null; |
| 127 | + const baseTokenId = body.data?.relationships?.base_token?.data?.id ?? ""; |
| 128 | + const wfairIsBaseToken = baseTokenId.toLowerCase().endsWith(WFAIR_ADDRESS.toLowerCase()); |
| 129 | + const wfairPriceUsd = wfairIsBaseToken ? attrs?.base_token_price_usd : attrs?.quote_token_price_usd; |
116 | 130 |
|
117 | 131 | return { |
118 | | - price: parseNumeric(attrs?.price_usd), |
| 132 | + price: parseNumeric(wfairPriceUsd), |
119 | 133 | context: { |
120 | | - change24h: null, |
| 134 | + change24h: parseNumeric(attrs?.price_change_percentage?.h24), |
121 | 135 | volume24h: parseNumeric(attrs?.volume_usd?.h24), |
122 | | - liquidityUsd: parseNumeric(attrs?.total_reserve_in_usd), |
| 136 | + liquidityUsd: parseNumeric(attrs?.reserve_in_usd), |
123 | 137 | marketCapUsd: parseNumeric(attrs?.market_cap_usd), |
124 | 138 | }, |
125 | 139 | }; |
|
0 commit comments