${msg}`)
+ markAlerted(pairKey, profitBps)
+ }
+ }
+ }
+ }
+
+ await scan()
+ setInterval(scan, ARB_MONITOR_INTERVAL_MS)
+}
+
+// ─── Summary for liquidity.js ─────────────────────────────────────────────────
+
+async function getArbSummaryForPair(symbolA, symbolB) {
+ const result = await checkPairArb(symbolA, symbolB)
+ if (!result) return null
+ return formatArbResult(
+ result.best.arb,
+ result.symbolA,
+ result.symbolB,
+ result.best.usdTradeSize
+ )
+}
+
+// ─── Exports ──────────────────────────────────────────────────────────────────
+
+export { checkArb, scanAllArb, monitorArb, getArbSummaryForPair }
diff --git a/skills/citrea-claw-skill/src/commands/balance.js b/skills/citrea-claw-skill/src/commands/balance.js
new file mode 100644
index 00000000..208f6d57
--- /dev/null
+++ b/skills/citrea-claw-skill/src/commands/balance.js
@@ -0,0 +1,153 @@
+import 'dotenv/config'
+import { createPublicClient, http, formatUnits } from 'viem'
+import { fetchRedStonePrices, formatUSD } from '../lib/prices.js'
+
+// ─── Citrea Mainnet Config ────────────────────────────────────────────────────
+const citrea = {
+ id: 4114,
+ name: 'Citrea Mainnet',
+ nativeCurrency: { name: 'Citrea Bitcoin', symbol: 'cBTC', decimals: 18 },
+ rpcUrls: { default: { http: ['https://rpc.mainnet.citrea.xyz'] } },
+ blockExplorers: {
+ default: {
+ name: 'Citrea Explorer',
+ url: 'https://explorer.mainnet.citrea.xyz'
+ }
+ }
+}
+
+// ─── Token Registry ───────────────────────────────────────────────────────────
+const TOKENS = [
+ { symbol: 'ctUSD', address: '0x8D82c4E3c936C7B5724A382a9c5a4E6Eb7aB6d5D', decimals: 18 },
+ { symbol: 'wcBTC', address: '0x3100000000000000000000000000000000000006', decimals: 18 },
+ { symbol: 'USDC.e', address: '0xE045e6c36cF77FAA2CfB54466D71A3aEF7bbE839', decimals: 6 },
+ { symbol: 'USDT.e', address: '0x9f3096Bac87e7F03DC09b0B416eB0DF837304dc4', decimals: 6 },
+ { symbol: 'WBTC.e', address: '0xDF240DC08B0FdaD1d93b74d5048871232f6BEA3d', decimals: 8 },
+ { symbol: 'JUSD', address: '0x0987D3720D38847ac6dBB9D025B9dE892a3CA35C', decimals: 18 },
+]
+
+const ERC20_ABI = [
+ {
+ name: 'balanceOf',
+ type: 'function',
+ stateMutability: 'view',
+ inputs: [{ name: 'account', type: 'address' }],
+ outputs: [{ name: '', type: 'uint256' }],
+ },
+]
+
+const client = createPublicClient({
+ chain: citrea,
+ transport: http()
+})
+
+// ─── Helpers ──────────────────────────────────────────────────────────────────
+
+function formatTokenAmount(n) {
+ if (n === 0) return '0'
+ if (n < 0.0001) return n.toFixed(8)
+ if (n < 1) return n.toFixed(6)
+ if (n < 1000) return n.toFixed(4)
+ return n.toLocaleString('en-US', { maximumFractionDigits: 2 })
+}
+
+function getUSDPrice(symbol, prices) {
+ const map = {
+ 'cBTC': prices?.BTC,
+ 'wcBTC': prices?.BTC,
+ 'WBTC.e': prices?.BTC,
+ 'USDC.e': prices?.USDC,
+ 'USDT.e': prices?.USDT,
+ 'ctUSD': prices?.USDC,
+ 'JUSD': prices?.USDC,
+ }
+ return map[symbol] || null
+}
+
+// ─── Wallet Balance ───────────────────────────────────────────────────────────
+
+export async function walletBalance(args) {
+ if (!args[0]) {
+ console.log(`
+Usage:
+ balance Show cBTC + token balances for a wallet
+
+Examples:
+ node index.js balance 0xYourAddress
+ `)
+ return
+ }
+
+ const address = args[0]
+
+ if (!address.startsWith('0x') || address.length !== 42) {
+ console.log(`❌ Invalid address "${address}" — must be a 42-character hex address`)
+ return
+ }
+
+ console.log(`\n🔍 Fetching balances for ${address}...`)
+
+ const [nativeBalance, ...tokenBalances] = await Promise.all([
+ client.getBalance({ address }),
+ ...TOKENS.map(t =>
+ client.readContract({
+ address: t.address,
+ abi: ERC20_ABI,
+ functionName: 'balanceOf',
+ args: [address],
+ }).catch(() => 0n)
+ ),
+ ])
+
+ const prices = await fetchRedStonePrices()
+
+ const rows = []
+ let totalUSD = 0
+
+ // Native cBTC
+ const cbtcAmount = Number(formatUnits(nativeBalance, 18))
+ const cbtcPrice = getUSDPrice('cBTC', prices)
+ const cbtcUSD = cbtcPrice ? cbtcAmount * cbtcPrice : null
+ if (cbtcUSD) totalUSD += cbtcUSD
+ rows.push({
+ symbol: 'cBTC',
+ amount: cbtcAmount,
+ usd: cbtcUSD,
+ nonZero: cbtcAmount > 0,
+ })
+
+ // ERC20 tokens
+ for (let i = 0; i < TOKENS.length; i++) {
+ const token = TOKENS[i]
+ const raw = tokenBalances[i]
+ const amount = Number(formatUnits(raw, token.decimals))
+ const price = getUSDPrice(token.symbol, prices)
+ const usd = price ? amount * price : null
+ if (usd) totalUSD += usd
+ rows.push({
+ symbol: token.symbol,
+ amount,
+ usd,
+ nonZero: amount > 0,
+ })
+ }
+
+ console.log(`
+💼 Wallet Balance
+━━━━━━━━━━━━━━━━━━━━━━━━
+ Address: ${address}
+ Network: Citrea Mainnet
+━━━━━━━━━━━━━━━━━━━━━━━━`)
+
+ for (const row of rows) {
+ const amountStr = formatTokenAmount(row.amount).padEnd(20)
+ const usdStr = row.usd != null ? formatUSD(row.usd) : ''
+ const zeroMark = row.nonZero ? ' ' : ' ·'
+ console.log(`${zeroMark} ${row.symbol.padEnd(8)} ${amountStr} ${usdStr}`)
+ }
+
+ console.log(`━━━━━━━━━━━━━━━━━━━━━━━━`)
+ console.log(` Total value: ${formatUSD(totalUSD)}`)
+ console.log(` Explorer: https://explorer.mainnet.citrea.xyz/address/${address}`)
+ console.log()
+}
diff --git a/skills/citrea-claw-skill/src/commands/liquidity.js b/skills/citrea-claw-skill/src/commands/liquidity.js
new file mode 100644
index 00000000..eac784dc
--- /dev/null
+++ b/skills/citrea-claw-skill/src/commands/liquidity.js
@@ -0,0 +1,419 @@
+import { createPublicClient, http, formatUnits } from 'viem'
+import { fetchRedStonePrices, formatUSD } from '../lib/prices.js'
+import { getArbSummaryForPair } from './arb.js'
+
+// ─── Citrea Mainnet Config ────────────────────────────────────────────────────
+const citrea = {
+ id: 4114,
+ name: 'Citrea Mainnet',
+ nativeCurrency: { name: 'Citrea Bitcoin', symbol: 'cBTC', decimals: 18 },
+ rpcUrls: { default: { http: ['https://rpc.mainnet.citrea.xyz'] } },
+ blockExplorers: {
+ default: {
+ name: 'Citrea Explorer',
+ url: 'https://explorer.mainnet.citrea.xyz'
+ }
+ }
+}
+
+// ─── Contract Addresses ───────────────────────────────────────────────────────
+const JUICESWAP_FACTORY = '0xd809b1285aDd8eeaF1B1566Bf31B2B4C4Bba8e82'
+const SATSUMA_FACTORY = '0x10253594A832f967994b44f33411940533302ACb'
+const EXPLORER_ADDR = 'https://explorer.mainnet.citrea.xyz/address'
+
+// ─── Token Registry ───────────────────────────────────────────────────────────
+const TOKEN_REGISTRY = {
+ 'ctusd': { symbol: 'ctUSD', address: '0x8D82c4E3c936C7B5724A382a9c5a4E6Eb7aB6d5D', decimals: 18 },
+ 'wcbtc': { symbol: 'wcBTC', address: '0x3100000000000000000000000000000000000006', decimals: 18 },
+ 'usdc.e': { symbol: 'USDC.e', address: '0xE045e6c36cF77FAA2CfB54466D71A3aEF7bbE839', decimals: 6 },
+ 'usdc': { symbol: 'USDC.e', address: '0xE045e6c36cF77FAA2CfB54466D71A3aEF7bbE839', decimals: 6 },
+ 'usdt.e': { symbol: 'USDT.e', address: '0x9f3096Bac87e7F03DC09b0B416eB0DF837304dc4', decimals: 6 },
+ 'usdt': { symbol: 'USDT.e', address: '0x9f3096Bac87e7F03DC09b0B416eB0DF837304dc4', decimals: 6 },
+ 'wbtc.e': { symbol: 'WBTC.e', address: '0xDF240DC08B0FdaD1d93b74d5048871232f6BEA3d', decimals: 8 },
+ 'wbtc': { symbol: 'WBTC.e', address: '0xDF240DC08B0FdaD1d93b74d5048871232f6BEA3d', decimals: 8 },
+}
+
+const ALL_TOKENS = [
+ { symbol: 'ctUSD', address: '0x8D82c4E3c936C7B5724A382a9c5a4E6Eb7aB6d5D', decimals: 18 },
+ { symbol: 'wcBTC', address: '0x3100000000000000000000000000000000000006', decimals: 18 },
+ { symbol: 'USDC.e', address: '0xE045e6c36cF77FAA2CfB54466D71A3aEF7bbE839', decimals: 6 },
+ { symbol: 'USDT.e', address: '0x9f3096Bac87e7F03DC09b0B416eB0DF837304dc4', decimals: 6 },
+ { symbol: 'WBTC.e', address: '0xDF240DC08B0FdaD1d93b74d5048871232f6BEA3d', decimals: 8 },
+]
+
+// ─── ABIs ─────────────────────────────────────────────────────────────────────
+
+const ERC20_ABI = [
+ {
+ name: 'balanceOf',
+ type: 'function',
+ stateMutability: 'view',
+ inputs: [{ name: 'account', type: 'address' }],
+ outputs: [{ name: '', type: 'uint256' }],
+ },
+ {
+ name: 'symbol',
+ type: 'function',
+ stateMutability: 'view',
+ inputs: [],
+ outputs: [{ name: '', type: 'string' }],
+ },
+ {
+ name: 'decimals',
+ type: 'function',
+ stateMutability: 'view',
+ inputs: [],
+ outputs: [{ name: '', type: 'uint8' }],
+ },
+]
+
+const POOL_ABI = [
+ { name: 'token0', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'address' }] },
+ { name: 'token1', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'address' }] },
+ { name: 'liquidity', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'uint128' }] },
+ { name: 'fee', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'uint24' }] },
+]
+
+const ALGEBRA_POOL_ABI = [
+ { name: 'token0', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'address' }] },
+ { name: 'token1', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'address' }] },
+ { name: 'liquidity', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'uint128' }] },
+ {
+ name: 'globalState',
+ type: 'function',
+ stateMutability: 'view',
+ inputs: [],
+ outputs: [
+ { name: 'sqrtPriceX96', type: 'uint160' },
+ { name: 'tick', type: 'int24' },
+ { name: 'fee', type: 'uint16' },
+ ],
+ },
+]
+
+const UNIV3_FACTORY_ABI = [
+ {
+ name: 'getPool',
+ type: 'function',
+ stateMutability: 'view',
+ inputs: [
+ { name: 'tokenA', type: 'address' },
+ { name: 'tokenB', type: 'address' },
+ { name: 'fee', type: 'uint24' },
+ ],
+ outputs: [{ name: 'pool', type: 'address' }],
+ },
+]
+
+const ALGEBRA_FACTORY_ABI = [
+ {
+ name: 'poolByPair',
+ type: 'function',
+ stateMutability: 'view',
+ inputs: [
+ { name: 'tokenA', type: 'address' },
+ { name: 'tokenB', type: 'address' },
+ ],
+ outputs: [{ name: 'pool', type: 'address' }],
+ },
+]
+
+const client = createPublicClient({
+ chain: citrea,
+ transport: http()
+})
+
+// ─── Token Resolver ───────────────────────────────────────────────────────────
+
+function resolveToken(input) {
+ if (input.startsWith('0x') && input.length === 42) {
+ return { address: input, symbol: input.slice(0, 8) + '...', decimals: 18 }
+ }
+
+ const match = TOKEN_REGISTRY[input.toLowerCase()]
+ if (match) return match
+
+ const fuzzy = ALL_TOKENS.filter(t =>
+ t.symbol.toLowerCase().includes(input.toLowerCase())
+ )
+
+ if (fuzzy.length === 1) return fuzzy[0]
+ if (fuzzy.length > 1) return { suggestions: fuzzy }
+
+ return null
+}
+
+function printSuggestions(input, suggestions) {
+ console.log(`\n❓ "${input}" didn't match any known token. Did you mean:\n`)
+ for (const t of suggestions) {
+ console.log(` ${t.symbol.padEnd(8)} ${t.address}`)
+ }
+ console.log(`\nOr use the full token address directly.`)
+ console.log(`\nKnown tokens on Citrea:`)
+ for (const t of ALL_TOKENS) {
+ console.log(` ${t.symbol.padEnd(8)} ${t.address}`)
+ }
+}
+
+// ─── Helpers ──────────────────────────────────────────────────────────────────
+
+async function getTokenInfo(address) {
+ const lower = address.toLowerCase()
+ const known = Object.values(TOKEN_REGISTRY).find(
+ t => t.address.toLowerCase() === lower
+ )
+ if (known) return known
+ try {
+ const [symbol, decimals] = await Promise.all([
+ client.readContract({ address, abi: ERC20_ABI, functionName: 'symbol' }),
+ client.readContract({ address, abi: ERC20_ABI, functionName: 'decimals' }),
+ ])
+ return { symbol, decimals }
+ } catch {
+ return { symbol: address.slice(0, 8) + '...', decimals: 18 }
+ }
+}
+
+function formatFee(fee) {
+ return (fee / 10000).toFixed(2) + '%'
+}
+
+function formatAmount(amount, decimals) {
+ const n = parseFloat(formatUnits(amount, decimals))
+ if (n === 0) return '0'
+ if (n < 0.000001) return '0 (dust)'
+ if (n < 1) return n.toFixed(6)
+ if (n < 1000) return n.toFixed(4)
+ return n.toLocaleString('en-US', { maximumFractionDigits: 2 })
+}
+
+// ─── Pool Lookup ──────────────────────────────────────────────────────────────
+
+async function findPool(tokenA, tokenB, fee = null) {
+ const results = []
+ const feeTiers = fee ? [fee] : [500, 3000, 10000]
+
+ for (const f of feeTiers) {
+ try {
+ const pool = await client.readContract({
+ address: JUICESWAP_FACTORY,
+ abi: UNIV3_FACTORY_ABI,
+ functionName: 'getPool',
+ args: [tokenA, tokenB, f],
+ })
+ if (pool !== '0x0000000000000000000000000000000000000000') {
+ results.push({ dex: 'JuiceSwap', pool, type: 'univ3' })
+ }
+ } catch {}
+ }
+
+ try {
+ const pool = await client.readContract({
+ address: SATSUMA_FACTORY,
+ abi: ALGEBRA_FACTORY_ABI,
+ functionName: 'poolByPair',
+ args: [tokenA, tokenB],
+ })
+ if (pool !== '0x0000000000000000000000000000000000000000') {
+ results.push({ dex: 'Satsuma', pool, type: 'algebra' })
+ }
+ } catch {}
+
+ return results
+}
+
+// ─── Pool TVL ─────────────────────────────────────────────────────────────────
+
+async function getPoolTVL(poolAddress, poolType = 'univ3', dexName = '') {
+ try {
+ const abi = poolType === 'algebra' ? ALGEBRA_POOL_ABI : POOL_ABI
+
+ const [token0Addr, token1Addr, liquidity] = await Promise.all([
+ client.readContract({ address: poolAddress, abi, functionName: 'token0' }),
+ client.readContract({ address: poolAddress, abi, functionName: 'token1' }),
+ client.readContract({ address: poolAddress, abi, functionName: 'liquidity' }),
+ ])
+
+ const [token0, token1] = await Promise.all([
+ getTokenInfo(token0Addr),
+ getTokenInfo(token1Addr),
+ ])
+
+ let fee
+ if (poolType === 'algebra') {
+ const state = await client.readContract({
+ address: poolAddress,
+ abi,
+ functionName: 'globalState',
+ })
+ fee = state[2]
+ } else {
+ fee = await client.readContract({
+ address: poolAddress,
+ abi,
+ functionName: 'fee',
+ })
+ }
+
+ const [balance0Raw, balance1Raw] = await Promise.all([
+ client.readContract({
+ address: token0Addr,
+ abi: ERC20_ABI,
+ functionName: 'balanceOf',
+ args: [poolAddress],
+ }),
+ client.readContract({
+ address: token1Addr,
+ abi: ERC20_ABI,
+ functionName: 'balanceOf',
+ args: [poolAddress],
+ }),
+ ])
+
+ const liquidityStatus = liquidity === 0n
+ ? '0 (no active liquidity in current price range)'
+ : liquidity.toLocaleString()
+
+ const lines = [
+ `💧 Pool TVL${dexName ? ` — ${dexName}` : ''}`,
+ `━━━━━━━━━━━━━━━━━━━━━━━━`,
+ ` Pair: ${token0.symbol} / ${token1.symbol}`,
+ ` Fee: ${poolType === 'algebra'
+ ? (Number(fee) / 10000).toFixed(4) + '%'
+ : formatFee(fee)}`,
+ ` Type: ${poolType === 'algebra' ? 'Algebra (Satsuma)' : 'Uniswap V3 (JuiceSwap)'}`,
+ ``,
+ `📊 Token Reserves`,
+ ` ${token0.symbol.padEnd(8)} ${formatAmount(balance0Raw, token0.decimals)}`,
+ ` ${token1.symbol.padEnd(8)} ${formatAmount(balance1Raw, token1.decimals)}`,
+ ``,
+ `📈 Active Liquidity`,
+ ` ${liquidityStatus}`,
+ ``,
+ `🔍 ${EXPLORER_ADDR}/${poolAddress}`,
+ ]
+
+ console.log(lines.join('\n'))
+
+ } catch (error) {
+ console.error(`❌ Error fetching pool TVL: ${error.message}`)
+ }
+}
+
+// ─── Main Entry ───────────────────────────────────────────────────────────────
+
+async function poolLiquidity(args) {
+
+ // Usage 1: direct pool address
+ if (args[0]?.startsWith('0x') && args[0].length === 42) {
+ const poolType = args[1] || 'univ3'
+ await getPoolTVL(args[0], poolType)
+ return
+ }
+
+ // Usage 2: two token symbols or addresses
+ if (args[0] && args[1]) {
+ const resolvedA = resolveToken(args[0])
+ const resolvedB = resolveToken(args[1])
+
+ if (!resolvedA) {
+ console.log(`\n❌ Unknown token "${args[0]}"`)
+ console.log(`\nKnown tokens:`)
+ for (const t of ALL_TOKENS) console.log(` ${t.symbol.padEnd(8)} ${t.address}`)
+ return
+ }
+ if (resolvedA.suggestions) { printSuggestions(args[0], resolvedA.suggestions); return }
+
+ if (!resolvedB) {
+ console.log(`\n❌ Unknown token "${args[1]}"`)
+ console.log(`\nKnown tokens:`)
+ for (const t of ALL_TOKENS) console.log(` ${t.symbol.padEnd(8)} ${t.address}`)
+ return
+ }
+ if (resolvedB.suggestions) { printSuggestions(args[1], resolvedB.suggestions); return }
+
+ const fee = args[2] ? parseInt(args[2]) : null
+ console.log(`\n🔍 Looking up ${resolvedA.symbol} / ${resolvedB.symbol} pools...\n`)
+
+ const pools = await findPool(resolvedA.address, resolvedB.address, fee)
+
+ if (pools.length === 0) {
+ console.log(`❌ No pool found for ${resolvedA.symbol} / ${resolvedB.symbol}.`)
+ console.log(` Try a different fee tier: 500, 3000, or 10000`)
+ return
+ }
+
+ for (const { dex, pool, type } of pools) {
+ console.log(`🔶 ${dex}`)
+ await getPoolTVL(pool, type, dex)
+ console.log()
+ }
+
+ // Check for arb opportunity across the pools found
+ const arbSummary = await getArbSummaryForPair(resolvedA.symbol, resolvedB.symbol)
+ if (arbSummary) console.log(arbSummary)
+ return
+ }
+
+ // Usage 3: single token — find all pools containing it
+ if (args[0]) {
+ const resolved = resolveToken(args[0])
+
+ if (!resolved) {
+ console.log(`\n❌ Unknown token "${args[0]}"`)
+ console.log(`\nKnown tokens:`)
+ for (const t of ALL_TOKENS) console.log(` ${t.symbol.padEnd(8)} ${t.address}`)
+ return
+ }
+ if (resolved.suggestions) { printSuggestions(args[0], resolved.suggestions); return }
+
+ console.log(`\n🔍 Finding all pools containing ${resolved.symbol}...\n`)
+
+ const otherTokens = ALL_TOKENS.filter(
+ t => t.address.toLowerCase() !== resolved.address.toLowerCase()
+ )
+
+ let foundAny = false
+
+ for (const other of otherTokens) {
+ const pools = await findPool(resolved.address, other.address, null)
+ for (const { dex, pool, type } of pools) {
+ console.log(`🔶 ${dex} — ${resolved.symbol} / ${other.symbol}`)
+ await getPoolTVL(pool, type, dex)
+ console.log()
+ foundAny = true
+ }
+
+ // Check for arb on this pair
+ if (foundAny) {
+ const arbSummary = await getArbSummaryForPair(resolved.symbol, other.symbol)
+ if (arbSummary) console.log(arbSummary)
+ }
+ }
+
+ if (!foundAny) {
+ console.log(`❌ No pools found containing ${resolved.symbol}.`)
+ }
+ return
+ }
+
+ // No args — show help
+ console.log(`
+Usage:
+ pool:liquidity ${msg}`)
+ }
+ }
+
+ lastCheckedBlock = latestBlock
+
+ } catch (err) {
+ console.error(`⚠️ Poll error: ${err.message}`)
+ }
+ }
+
+ await poll()
+ setInterval(poll, 10_000)
+}
+
+export { getRecentPools, getLatestPool, monitorPools }
diff --git a/skills/citrea-claw-skill/src/commands/prices-cli.js b/skills/citrea-claw-skill/src/commands/prices-cli.js
new file mode 100644
index 00000000..de545e34
--- /dev/null
+++ b/skills/citrea-claw-skill/src/commands/prices-cli.js
@@ -0,0 +1,374 @@
+import 'dotenv/config'
+import { createPublicClient, http } from 'viem'
+import { fetchRedStonePrices, formatUSD } from '../lib/prices.js'
+
+const citrea = {
+ id: 4114,
+ name: 'Citrea Mainnet',
+ nativeCurrency: { name: 'Citrea Bitcoin', symbol: 'cBTC', decimals: 18 },
+ rpcUrls: { default: { http: ['https://rpc.mainnet.citrea.xyz'] } },
+}
+
+const client = createPublicClient({ chain: citrea, transport: http() })
+
+const ALL_TOKENS = [
+ { symbol: 'ctUSD', address: '0x8D82c4E3c936C7B5724A382a9c5a4E6Eb7aB6d5D', decimals: 18 },
+ { symbol: 'wcBTC', address: '0x3100000000000000000000000000000000000006', decimals: 18 },
+ { symbol: 'USDC.e', address: '0xE045e6c36cF77FAA2CfB54466D71A3aEF7bbE839', decimals: 6 },
+ { symbol: 'USDT.e', address: '0x9f3096Bac87e7F03DC09b0B416eB0DF837304dc4', decimals: 6 },
+ { symbol: 'WBTC.e', address: '0xDF240DC08B0FdaD1d93b74d5048871232f6BEA3d', decimals: 8 },
+ { symbol: 'JUSD', address: '0x0987D3720D38847ac6dBB9D025B9dE892a3CA35C', decimals: 18 },
+]
+
+const BTC_TOKENS = ['wcBTC', 'WBTC.e', 'cBTC']
+const STABLE_TOKENS = ['USDC.e', 'USDT.e', 'ctUSD', 'JUSD']
+
+const JUICESWAP_FACTORY = '0xd809b1285aDd8eeaF1B1566Bf31B2B4C4Bba8e82'
+const SATSUMA_FACTORY = '0x10253594A832f967994b44f33411940533302ACb'
+
+const POOL_ADDRESS_OVERRIDE = {
+ '0x0987d3720d38847ac6dbb9d025b9de892a3ca35c': '0x1b70ae756b1089cc5948e4f8a2AD498DF30E897d',
+}
+
+function getPoolQueryAddress(address) {
+ return POOL_ADDRESS_OVERRIDE[address.toLowerCase()] || address
+}
+
+// ─── ABIs ─────────────────────────────────────────────────────────────────────
+
+const UNIV3_FACTORY_ABI = [
+ {
+ name: 'getPool',
+ type: 'function',
+ stateMutability: 'view',
+ inputs: [
+ { name: 'tokenA', type: 'address' },
+ { name: 'tokenB', type: 'address' },
+ { name: 'fee', type: 'uint24' },
+ ],
+ outputs: [{ name: 'pool', type: 'address' }],
+ },
+]
+
+const ALGEBRA_FACTORY_ABI = [
+ {
+ name: 'poolByPair',
+ type: 'function',
+ stateMutability: 'view',
+ inputs: [
+ { name: 'tokenA', type: 'address' },
+ { name: 'tokenB', type: 'address' },
+ ],
+ outputs: [{ name: 'pool', type: 'address' }],
+ },
+]
+
+const SLOT0_ABI = [
+ {
+ name: 'slot0',
+ type: 'function',
+ stateMutability: 'view',
+ inputs: [],
+ outputs: [
+ { name: 'sqrtPriceX96', type: 'uint160' },
+ { name: 'tick', type: 'int24' },
+ ],
+ },
+ { name: 'fee', type: 'function', stateMutability: 'view', inputs: [], outputs: [{ name: '', type: 'uint24' }] },
+]
+
+const GLOBAL_STATE_ABI = [
+ {
+ name: 'globalState',
+ type: 'function',
+ stateMutability: 'view',
+ inputs: [],
+ outputs: [
+ { name: 'sqrtPriceX96', type: 'uint160' },
+ { name: 'tick', type: 'int24' },
+ { name: 'fee', type: 'uint16' },
+ ],
+ },
+]
+
+// ─── Helpers ──────────────────────────────────────────────────────────────────
+
+function resolveToken(input) {
+ const lower = input.toLowerCase()
+ return ALL_TOKENS.find(t =>
+ t.symbol.toLowerCase() === lower ||
+ t.address.toLowerCase() === lower
+ ) || null
+}
+
+function sqrtPriceX96ToRaw(sqrtPriceX96) {
+ const Q96 = 2 ** 96
+ const sqrtFloat = Number(sqrtPriceX96) / Q96
+ return sqrtFloat * sqrtFloat
+}
+
+// Convert raw price to human price, with sanity checking.
+// Some pools encode human price directly, others encode raw token ratios.
+// We detect which by checking if the result is in a sensible range.
+// Returns null if the pool appears to be uninitialized or badly initialized.
+function adjustForDecimals(rawPrice, token0, token1) {
+ const isBtc0 = BTC_TOKENS.includes(token0.symbol)
+ const isBtc1 = BTC_TOKENS.includes(token1.symbol)
+ const isStable0 = STABLE_TOKENS.includes(token0.symbol)
+ const isStable1 = STABLE_TOKENS.includes(token1.symbol)
+
+ if (token0.decimals === token1.decimals) {
+ // Same decimals — raw price = human price
+ // Sanity check for BTC/BTC: should be near 1.0
+ if (isBtc0 && isBtc1) {
+ if (rawPrice > 1000 || rawPrice < 0.001) return null // badly initialized
+ }
+ // Sanity check for stable/stable: should be near 1.0
+ if (isStable0 && isStable1) {
+ if (rawPrice > 1000 || rawPrice < 0.001) return null
+ }
+ return rawPrice
+ }
+
+ // Different decimals — try adjustment
+ const adjusted = rawPrice * (10 ** token0.decimals) / (10 ** token1.decimals)
+
+ if (isStable0 && isStable1) {
+ // stable/stable should be near 1.0
+ if (adjusted > 1000 || adjusted < 0.001) return rawPrice // pool uses human price
+ return adjusted
+ }
+
+ if ((isBtc0 && isStable1) || (isStable0 && isBtc1)) {
+ // BTC/stable: expect 1,000–500,000 range
+ if (adjusted >= 1000 && adjusted <= 1e8) return adjusted // adjustment worked
+ if (rawPrice >= 1000 && rawPrice <= 1e8) return rawPrice // pool uses human price
+ return null // neither makes sense
+ }
+
+ return adjusted
+}
+
+function formatPrice(price, symbol) {
+ if (STABLE_TOKENS.includes(symbol)) return formatUSD(price)
+ if (price < 0.0001) return price.toFixed(10)
+ if (price < 1) return price.toFixed(6)
+ if (price < 1000) return price.toFixed(4)
+ return price.toLocaleString('en-US', { maximumFractionDigits: 2 })
+}
+
+function getOraclePrice(symbol, prices) {
+ const map = {
+ 'cBTC': prices?.BTC,
+ 'wcBTC': prices?.BTC,
+ 'WBTC.e': prices?.BTC,
+ 'USDC.e': prices?.USDC,
+ 'USDT.e': prices?.USDT,
+ 'ctUSD': prices?.USDC,
+ 'JUSD': prices?.USDC,
+ }
+ return map[symbol] || null
+}
+
+// Returns { baseSymbol, quoteSymbol, displayPrice } or null if pool is invalid
+function computeDisplayPrice(sqrtPriceX96, tokenA, tokenB, queryAddrA, queryAddrB) {
+ const token0IsA = queryAddrA.toLowerCase() < queryAddrB.toLowerCase()
+ const token0 = token0IsA ? tokenA : tokenB
+ const token1 = token0IsA ? tokenB : tokenA
+
+ const rawPrice = sqrtPriceX96ToRaw(sqrtPriceX96)
+ const humanPrice = adjustForDecimals(rawPrice, token0, token1)
+
+ if (humanPrice === null) return null
+ // humanPrice = token1 per token0 in human units
+
+ const isBtcA = BTC_TOKENS.includes(tokenA.symbol)
+ const isBtcB = BTC_TOKENS.includes(tokenB.symbol)
+
+ if (isBtcA && !isBtcB) {
+ // want: 1 BTC = X stable
+ if (token0IsA) {
+ // token0=BTC → humanPrice = stable/BTC ✓
+ return { baseSymbol: tokenA.symbol, quoteSymbol: tokenB.symbol, displayPrice: humanPrice }
+ } else {
+ // token0=stable → humanPrice = BTC/stable → invert
+ return { baseSymbol: tokenA.symbol, quoteSymbol: tokenB.symbol, displayPrice: 1 / humanPrice }
+ }
+ } else if (!isBtcA && isBtcB) {
+ // want: 1 BTC = X stable (tokenB is BTC)
+ if (token0IsA) {
+ // token0=stable → humanPrice = BTC/stable → invert → stable/BTC
+ return { baseSymbol: tokenB.symbol, quoteSymbol: tokenA.symbol, displayPrice: 1 / humanPrice }
+ } else {
+ // token0=BTC → humanPrice = stable/BTC ✓
+ return { baseSymbol: tokenB.symbol, quoteSymbol: tokenA.symbol, displayPrice: humanPrice }
+ }
+ } else {
+ // stable/stable or BTC/BTC → 1 tokenA = X tokenB
+ if (token0IsA) {
+ // token0=tokenA → humanPrice = tokenB/tokenA ✓
+ return { baseSymbol: tokenA.symbol, quoteSymbol: tokenB.symbol, displayPrice: humanPrice }
+ } else {
+ // token0=tokenB → humanPrice = tokenA/tokenB → invert
+ return { baseSymbol: tokenA.symbol, quoteSymbol: tokenB.symbol, displayPrice: 1 / humanPrice }
+ }
+ }
+}
+
+// ─── Price Command ────────────────────────────────────────────────────────────
+
+export async function priceCheck(args) {
+ if (!args[0]) {
+ console.log(`
+Usage:
+ price | + | |