From 4c5720d441b2908563ff4bec9f964028845c979d Mon Sep 17 00:00:00 2001 From: Landyn Date: Wed, 13 May 2026 16:06:48 -0500 Subject: [PATCH] fix(agents): correct best-rate selection and conversion for TAO->BTC quote Both rate (BTC->TAO) and counterRate (TAO->BTC) are stored as TAO per 1 BTC, so the "best" miner flips by direction: forward picks the highest rate (most TAO out per BTC in), reverse picks the lowest counterRate (least TAO in per BTC out). The helper picked max in both cases and always multiplied amount by rate, which produced absurd outputs like "1 TAO -> 324 BTC". Display the effective rate (dest per source) and divide for the reverse leg. Update the curl + jq snippet so its sort direction matches, and add a rate-semantics note to the agent markdown so LLM consumers don't repeat the mistake. --- src/components/agents/AgentMarkdown.ts | 9 ++++ src/components/agents/RateQuoteHelper.tsx | 66 +++++++++++++++++------ 2 files changed, 60 insertions(+), 15 deletions(-) diff --git a/src/components/agents/AgentMarkdown.ts b/src/components/agents/AgentMarkdown.ts index 88a66f8..d5f8279 100644 --- a/src/components/agents/AgentMarkdown.ts +++ b/src/components/agents/AgentMarkdown.ts @@ -338,6 +338,15 @@ live state instead of polling, and don't hammer it. updatedAt: string; }; +> Rate semantics: both \`rate\` (sourceChain→destChain) and \`counterRate\` +> (destChain→sourceChain) are quoted in **canonical_dest per 1 +> canonical_source** — for a (btc, tao) miner that's **TAO per 1 BTC** for +> both fields. So picking the best miner *flips* by direction: +> BTC→TAO wants the **highest** \`rate\` (most TAO out per BTC in); TAO→BTC +> wants the **lowest** \`counterRate\` (least TAO needed per BTC received). +> To compute output: BTC→TAO → \`tao_out = btc_in * rate\`; TAO→BTC → +> \`btc_out = tao_in / counterRate\`. + type ActiveSwap = { swapId: string; status: 'ACTIVE' | 'FULFILLED' | 'COMPLETED' | 'TIMED_OUT'; diff --git a/src/components/agents/RateQuoteHelper.tsx b/src/components/agents/RateQuoteHelper.tsx index eda9c17..b30442c 100644 --- a/src/components/agents/RateQuoteHelper.tsx +++ b/src/components/agents/RateQuoteHelper.tsx @@ -14,14 +14,19 @@ import { FONTS } from '../../theme'; import { useMiners } from '../../api'; import { useCopy } from '../../hooks'; import HoverCard from '../HoverCard'; -import { formatRate } from '../../utils/format'; +import { formatRate, trimTrailingZeros } from '../../utils/format'; type Direction = 'BTC->TAO' | 'TAO->BTC'; interface BestQuote { uid: number; hotkey: string; - rate: string; + // Raw rate from the API, always expressed as TAO per 1 BTC regardless of + // direction (canonical_dest per 1 canonical_source — see Miner model docs). + rawRate: string; + // Effective rate for the user's chosen direction (destSym per 1 sourceSym). + // For BTC->TAO that's `rawRate`; for TAO->BTC it's `1 / rawRate`. + effectiveRate: number; out: string; } @@ -38,30 +43,53 @@ const computeBest = ( direction: Direction, amount: number, ): BestQuote | null => { - // Canonical ordering: API returns sourceChain='btc', destChain='tao' (lowercase). - // `rate` is the BTC->TAO quote; `counterRate` is the TAO->BTC quote when - // posted. Filter on a case-insensitive btc/tao match so a future casing - // change on the API side doesn't silently zero this out again. + // Canonical ordering: API returns sourceChain='btc', destChain='tao' + // (lowercase). Both `rate` (BTC->TAO) and `counterRate` (TAO->BTC) are in + // the same canonical unit: TAO per 1 BTC. So the "best" deal flips: + // - BTC->TAO: highest rate (most TAO per BTC sold) + // - TAO->BTC: lowest counterRate (least TAO to buy 1 BTC) + // Filter case-insensitively so a future casing change on the API doesn't + // silently zero this out again. + const isForward = direction === 'BTC->TAO'; const candidates = miners .filter((m) => m.isActive) .map((m) => { const src = (m.sourceChain ?? '').toLowerCase(); const dst = (m.destChain ?? '').toLowerCase(); if (src !== 'btc' || dst !== 'tao') return null; - const r = direction === 'BTC->TAO' ? m.rate : m.counterRate; - if (!r || parseFloat(r) <= 0) return null; - return { uid: m.uid, hotkey: m.hotkey, rate: r }; + const r = isForward ? m.rate : m.counterRate; + if (!r) return null; + const parsed = parseFloat(r); + if (!isFinite(parsed) || parsed <= 0) return null; + return { uid: m.uid, hotkey: m.hotkey, rawRate: r, parsed }; }) .filter( - (x): x is { uid: number; hotkey: string; rate: string } => x !== null, + ( + x, + ): x is { + uid: number; + hotkey: string; + rawRate: string; + parsed: number; + } => x !== null, ); if (candidates.length === 0) return null; const best = candidates.reduce((a, b) => - parseFloat(a.rate) >= parseFloat(b.rate) ? a : b, + isForward ? (a.parsed >= b.parsed ? a : b) : a.parsed <= b.parsed ? a : b, ); - const out = (parseFloat(best.rate) * amount).toFixed(6); - return { ...best, out }; + const effectiveRate = isForward ? best.parsed : 1 / best.parsed; + // 8 decimals covers BTC's smallest unit so a small TAO->BTC quote still + // renders with usable precision instead of rounding to zero. Trim trailing + // zeros so a clean number doesn't display with eight padding digits. + const out = trimTrailingZeros((effectiveRate * amount).toFixed(8)); + return { + uid: best.uid, + hotkey: best.hotkey, + rawRate: best.rawRate, + effectiveRate, + out, + }; }; interface CopyRowProps { @@ -146,7 +174,14 @@ const RateQuoteHelper: React.FC = () => { ? `alw swap now --auto --yes --from ${fromArg} --to ${toArg} --amount ${amount} --receive-address --from-address ` : `# no active miner quoting ${sourceSym} -> ${destSym} right now`; - const curlCmd = `curl -s https://api.all-ways.io/miners | jq '.[] | select(.isActive and (.sourceChain | ascii_downcase) == "btc" and (.destChain | ascii_downcase) == "tao") | {uid, rate: ${direction === 'BTC->TAO' ? '.rate' : '.counterRate'}, hotkey}' | jq -s 'sort_by(-(.rate | tonumber))[0]'`; + // Both `rate` (BTC->TAO) and `counterRate` (TAO->BTC) are stored as + // TAO per 1 BTC, so the "best" miner flips by direction: forward picks + // the highest (most TAO out per BTC in), reverse picks the lowest (least + // TAO in per BTC out). + const rateField = direction === 'BTC->TAO' ? '.rate' : '.counterRate'; + const sortExpr = + direction === 'BTC->TAO' ? '-(.rate | tonumber)' : '(.rate | tonumber)'; + const curlCmd = `curl -s https://api.all-ways.io/miners | jq '.[] | select(.isActive and (.sourceChain | ascii_downcase) == "btc" and (.destChain | ascii_downcase) == "tao") | {uid, rate: ${rateField}, hotkey}' | jq -s 'sort_by(${sortExpr})[0]'`; return ( { color: 'text.primary', }} > - {best ? formatRate(best.rate) : '—'} {destSym}/{sourceSym} + {best ? formatRate(best.effectiveRate) : '—'} {destSym}/ + {sourceSym}