Skip to content

Commit 8150444

Browse files
NateIsernclaude
andcommitted
fix(price): read indexed WFAIR price from GeckoTerminal pool endpoint
#11 switched the oracle off the manipulable Uniswap V3 slot0 spot price to GeckoTerminal, but queried the token endpoint — whose token-level price_usd is null for WFAIR — so /api/price returned null. Read the indexed price from the WFAIR/USDC pool endpoint instead (base_token_price_usd, with a base/quote guard), which is both available and not a raw on-chain spot read. Restores the ~0.49 price. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ebaffc3 commit 8150444

1 file changed

Lines changed: 29 additions & 15 deletions

File tree

server/lib/price-service.ts

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
//
33
// The live USD price of FAIR is sourced from WFAIR — the wrapped-FAIR bridge
44
// 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.)
810
//
911
// This module is the single source of truth for the price computation. Both the
1012
// REST route (`routes/price.ts`) and the MCP server consume `getPrice()` here so
@@ -13,6 +15,9 @@
1315
/** WFAIR on Base, 18 decimals. */
1416
const WFAIR_ADDRESS = "0xF2853CedDF47A05Fee0B4b24DFf2925d59737fb3" as const;
1517

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+
1621
const WFAIR_NETWORK = "base";
1722
export const PRICE_SOURCE_INDEXER = "wfair-base" as const;
1823
export type PriceSource = typeof PRICE_SOURCE_INDEXER;
@@ -53,14 +58,18 @@ const EMPTY_CONTEXT: PriceContext = {
5358

5459
// ---- Upstream response shapes (only the fields we consume) ----
5560

56-
interface GeckoTerminalTokenResponse {
61+
interface GeckoTerminalPoolResponse {
5762
data?: {
5863
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;
6167
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;
6473
} | null;
6574
} | null;
6675
}
@@ -105,21 +114,26 @@ async function fetchJson<T>(url: string): Promise<T> {
105114
}
106115

107116
/**
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.
110121
*/
111122
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}`,
114125
);
115126
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;
116130

117131
return {
118-
price: parseNumeric(attrs?.price_usd),
132+
price: parseNumeric(wfairPriceUsd),
119133
context: {
120-
change24h: null,
134+
change24h: parseNumeric(attrs?.price_change_percentage?.h24),
121135
volume24h: parseNumeric(attrs?.volume_usd?.h24),
122-
liquidityUsd: parseNumeric(attrs?.total_reserve_in_usd),
136+
liquidityUsd: parseNumeric(attrs?.reserve_in_usd),
123137
marketCapUsd: parseNumeric(attrs?.market_cap_usd),
124138
},
125139
};

0 commit comments

Comments
 (0)