@agether/agether 3.0.1 → 3.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +2 -2
  2. package/src/index.ts +319 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agether/agether",
3
- "version": "3.0.1",
3
+ "version": "3.1.4",
4
4
  "description": "OpenClaw plugin for Agether — onchain credit for AI agents on Ethereum & Base",
5
5
  "main": "src/index.ts",
6
6
  "openclaw": {
@@ -9,7 +9,7 @@
9
9
  ]
10
10
  },
11
11
  "dependencies": {
12
- "@agether/sdk": "^2.16.1",
12
+ "@agether/sdk": "file:../sdk",
13
13
  "axios": "^1.6.0",
14
14
  "ethers": "^6.9.0"
15
15
  },
package/src/index.ts CHANGED
@@ -1806,6 +1806,325 @@ export default function register(api: any) {
1806
1806
  },
1807
1807
  });
1808
1808
 
1809
+ // ═══════════════════════════════════════════════════════
1810
+ // TOOL: morpho_yield_spread
1811
+ // ═══════════════════════════════════════════════════════
1812
+ api.registerTool({
1813
+ name: "morpho_yield_spread",
1814
+ description: "Analyze yield spread between LST/LRT collateral yield and Morpho borrow rates. Shows profitable carry trades with real liquidity.",
1815
+ parameters: {
1816
+ type: "object",
1817
+ properties: {
1818
+ token: { type: "string", description: "Filter by collateral token (e.g. 'wstETH'). Omit for all LST markets." },
1819
+ minLiquidity: { type: "number", description: "Minimum market liquidity in USD (default: 10000). Set to 0 to include empty markets." },
1820
+ },
1821
+ },
1822
+ async execute(_id: string, params: { token?: string; minLiquidity?: number }) {
1823
+ try {
1824
+ const cfg = getConfig(api);
1825
+ requireChain(cfg);
1826
+ const client = createMorphoClient(cfg);
1827
+ const spreads = await client.getYieldSpread(params.minLiquidity ?? 10000);
1828
+ const filtered = params.token
1829
+ ? spreads.filter((s: any) => s.collateralToken.toLowerCase() === params.token!.toLowerCase())
1830
+ : spreads;
1831
+
1832
+ if (filtered.length === 0) {
1833
+ return ok(JSON.stringify({
1834
+ status: "no_lst_markets",
1835
+ message: params.token
1836
+ ? `No LST markets found for ${params.token}`
1837
+ : "No LST/LRT markets found on this chain",
1838
+ }));
1839
+ }
1840
+
1841
+ const summary = filtered.map((s: any) => ({
1842
+ collateral: s.collateralToken,
1843
+ loan: s.loanToken,
1844
+ collateralYield: `${s.collateralYield}%`,
1845
+ borrowRate: `${s.borrowRate.toFixed(2)}%`,
1846
+ netSpread: `${s.netSpread > 0 ? '+' : ''}${s.netSpread.toFixed(2)}%`,
1847
+ profitable: s.profitable ? '✅' : '❌',
1848
+ maxLeverage: `${s.maxSafeLeverage}x`,
1849
+ leveragedApy: `${s.leveragedNetApy > 0 ? '+' : ''}${s.leveragedNetApy}%`,
1850
+ liquidity: `$${(s.liquidity / 1e6).toFixed(1)}M`,
1851
+ }));
1852
+
1853
+ return ok(JSON.stringify({
1854
+ status: "yield_spread_analysis",
1855
+ markets: summary,
1856
+ bestOpportunity: filtered[0]
1857
+ ? `${filtered[0].collateralToken}/${filtered[0].loanToken}: ${filtered[0].netSpread > 0 ? '+' : ''}${filtered[0].netSpread.toFixed(2)}% spread (${filtered[0].leveragedNetApy}% at ${filtered[0].maxSafeLeverage}x leverage)`
1858
+ : "none",
1859
+ }));
1860
+ } catch (e) { return fail(e); }
1861
+ },
1862
+ });
1863
+
1864
+
1865
+
1866
+ // ═══════════════════════════════════════════════════════
1867
+ // TOOL: morpho_position_analysis
1868
+ // ═══════════════════════════════════════════════════════
1869
+ api.registerTool({
1870
+ name: "morpho_position_analysis",
1871
+ description: "Analyze position health over time. Shows current HF/LTV and projects future values based on LST yield vs borrow rate. Tracks historical snapshots.",
1872
+ parameters: {
1873
+ type: "object",
1874
+ properties: {
1875
+ token: { type: "string", description: "Collateral token to analyze (e.g. 'wstETH'). Omit for all positions." },
1876
+ },
1877
+ },
1878
+ async execute(_id: string, params: { token?: string }) {
1879
+ try {
1880
+ const cfg = getConfig(api);
1881
+ requireChain(cfg);
1882
+ const client = createMorphoClient(cfg);
1883
+
1884
+ // Get current positions and yield data
1885
+ const [status, spreads] = await Promise.all([
1886
+ client.getStatus(),
1887
+ client.getYieldSpread(),
1888
+ ]);
1889
+
1890
+ if (status.positions.length === 0) {
1891
+ return ok(JSON.stringify({
1892
+ status: "no_positions",
1893
+ message: "No active Morpho positions to analyze.",
1894
+ }));
1895
+ }
1896
+
1897
+ const analyses = [];
1898
+
1899
+ for (const pos of status.positions) {
1900
+ if (params.token && pos.collateralToken.toLowerCase() !== params.token.toLowerCase()) continue;
1901
+
1902
+ const collateralValue = parseFloat(pos.collateral);
1903
+ const debtValue = parseFloat(pos.debt);
1904
+ if (collateralValue === 0) continue;
1905
+
1906
+ // Find yield spread for this collateral
1907
+ const spread = spreads.find(
1908
+ (s: any) => s.collateralToken.toLowerCase() === pos.collateralToken.toLowerCase()
1909
+ && s.loanToken.toLowerCase() === pos.loanToken.toLowerCase()
1910
+ );
1911
+
1912
+ const collateralYield = spread?.collateralYield ?? 0;
1913
+ const borrowRate = spread?.borrowRate ?? 0;
1914
+ const lltv = spread?.lltv ?? 0.8;
1915
+
1916
+ // Current metrics
1917
+ const currentLtv = debtValue > 0 ? (debtValue / collateralValue) : 0;
1918
+ const currentHf = debtValue > 0 ? (collateralValue * lltv) / debtValue : Infinity;
1919
+
1920
+ // Project forward: how collateral and debt change over time
1921
+ // Collateral grows at yield rate, debt grows at borrow rate
1922
+ const projections = [30, 90, 180, 365].map(days => {
1923
+ const yearFraction = days / 365;
1924
+ const projectedCollateral = collateralValue * (1 + (collateralYield / 100) * yearFraction);
1925
+ const projectedDebt = debtValue * (1 + (borrowRate / 100) * yearFraction);
1926
+ const projectedLtv = projectedDebt > 0 ? projectedDebt / projectedCollateral : 0;
1927
+ const projectedHf = projectedDebt > 0 ? (projectedCollateral * lltv) / projectedDebt : Infinity;
1928
+ const yieldEarned = projectedCollateral - collateralValue;
1929
+ const interestPaid = projectedDebt - debtValue;
1930
+ const netProfit = yieldEarned - interestPaid;
1931
+
1932
+ return {
1933
+ days,
1934
+ collateral: projectedCollateral.toFixed(6),
1935
+ debt: projectedDebt.toFixed(6),
1936
+ ltv: (projectedLtv * 100).toFixed(2) + '%',
1937
+ healthFactor: projectedHf === Infinity ? '∞' : projectedHf.toFixed(3),
1938
+ yieldEarned: yieldEarned.toFixed(6),
1939
+ interestPaid: interestPaid.toFixed(6),
1940
+ netProfit: (netProfit >= 0 ? '+' : '') + netProfit.toFixed(6),
1941
+ ltvChange: ((currentLtv - projectedLtv) * 100).toFixed(3) + '% improvement',
1942
+ };
1943
+ });
1944
+
1945
+ // Calculate when LTV reaches key thresholds
1946
+ const timeToSafeLtv = currentLtv > 0.5 && collateralYield > borrowRate
1947
+ ? Math.ceil(((currentLtv - 0.5) / (currentLtv * (collateralYield - borrowRate) / 100)) * 365)
1948
+ : null;
1949
+
1950
+ // Max additional borrowable from yield growth
1951
+ const excessCollateralIn30d = collateralValue * (collateralYield / 100) * (30 / 365);
1952
+ const additionalBorrowable = excessCollateralIn30d * lltv;
1953
+
1954
+ analyses.push({
1955
+ collateralToken: pos.collateralToken,
1956
+ loanToken: pos.loanToken,
1957
+ current: {
1958
+ collateral: collateralValue.toFixed(6) + ' ' + pos.collateralToken,
1959
+ debt: debtValue.toFixed(6) + ' ' + pos.loanToken,
1960
+ ltv: (currentLtv * 100).toFixed(2) + '%',
1961
+ healthFactor: currentHf === Infinity ? '∞' : currentHf.toFixed(3),
1962
+ lltv: (lltv * 100).toFixed(0) + '%',
1963
+ },
1964
+ rates: {
1965
+ collateralYield: collateralYield + '% APY',
1966
+ borrowRate: borrowRate.toFixed(2) + '% APY',
1967
+ netSpread: (collateralYield - borrowRate > 0 ? '+' : '') + (collateralYield - borrowRate).toFixed(2) + '%',
1968
+ profitable: collateralYield > borrowRate,
1969
+ },
1970
+ projections,
1971
+ insights: {
1972
+ daysToSafeLtv50: timeToSafeLtv ? timeToSafeLtv + ' days' : 'already below or not applicable',
1973
+ additionalBorrowableIn30d: additionalBorrowable.toFixed(6) + ' ' + pos.loanToken,
1974
+ excessCollateralIn30d: excessCollateralIn30d.toFixed(6) + ' ' + pos.collateralToken,
1975
+ annualProfit: debtValue > 0
1976
+ ? ((collateralValue * collateralYield / 100) - (debtValue * borrowRate / 100)).toFixed(4) + ' net/year'
1977
+ : 'no debt',
1978
+ },
1979
+ });
1980
+ }
1981
+
1982
+ // Save snapshot for historical tracking
1983
+ try {
1984
+ const snapshotDir = path.join(
1985
+ process.env.HOME || "/root", ".openclaw", "agether-snapshots"
1986
+ );
1987
+ if (!fs.existsSync(snapshotDir)) fs.mkdirSync(snapshotDir, { recursive: true });
1988
+
1989
+ const snapshot = {
1990
+ timestamp: new Date().toISOString(),
1991
+ chainId: cfg.chainId,
1992
+ positions: analyses.map(a => ({
1993
+ collateralToken: a.collateralToken,
1994
+ loanToken: a.loanToken,
1995
+ ltv: a.current.ltv,
1996
+ healthFactor: a.current.healthFactor,
1997
+ collateral: a.current.collateral,
1998
+ debt: a.current.debt,
1999
+ })),
2000
+ };
2001
+
2002
+ const snapshotFile = path.join(snapshotDir, "snapshots.json");
2003
+ let snapshots: any[] = [];
2004
+ if (fs.existsSync(snapshotFile)) {
2005
+ try { snapshots = JSON.parse(fs.readFileSync(snapshotFile, "utf-8")); } catch {}
2006
+ }
2007
+ snapshots.push(snapshot);
2008
+ // Keep last 1000 snapshots
2009
+ if (snapshots.length > 1000) snapshots = snapshots.slice(-1000);
2010
+ fs.writeFileSync(snapshotFile, JSON.stringify(snapshots, null, 2));
2011
+ } catch {
2012
+ // Snapshot save failed — non-critical
2013
+ }
2014
+
2015
+ return ok(JSON.stringify({
2016
+ status: "position_analysis",
2017
+ positions: analyses,
2018
+ summary: analyses.length > 0
2019
+ ? analyses.map(a =>
2020
+ `${a.collateralToken}/${a.loanToken}: LTV ${a.current.ltv} → ${a.projections[3].ltv} in 1yr | ` +
2021
+ `HF ${a.current.healthFactor} → ${a.projections[3].healthFactor} | ` +
2022
+ `Net spread: ${a.rates.netSpread}`
2023
+ ).join('\n')
2024
+ : 'No positions match filter',
2025
+ }));
2026
+ } catch (e) { return fail(e); }
2027
+ },
2028
+ });
2029
+
2030
+ // ═══════════════════════════════════════════════════════
2031
+ // TOOL: morpho_position_history
2032
+ // ═══════════════════════════════════════════════════════
2033
+ api.registerTool({
2034
+ name: "morpho_position_history",
2035
+ description: "Show historical HF/LTV snapshots for positions. Tracks how yield has improved position health over time.",
2036
+ parameters: {
2037
+ type: "object",
2038
+ properties: {
2039
+ token: { type: "string", description: "Filter by collateral token (optional)" },
2040
+ days: { type: "number", description: "Number of days to look back (default: 30)" },
2041
+ },
2042
+ },
2043
+ async execute(_id: string, params: { token?: string; days?: number }) {
2044
+ try {
2045
+ const snapshotDir = path.join(
2046
+ process.env.HOME || "/root", ".openclaw", "agether-snapshots"
2047
+ );
2048
+ const snapshotFile = path.join(snapshotDir, "snapshots.json");
2049
+
2050
+ if (!fs.existsSync(snapshotFile)) {
2051
+ return ok(JSON.stringify({
2052
+ status: "no_history",
2053
+ message: "No snapshots yet. Run morpho_position_analysis first to start tracking.",
2054
+ }));
2055
+ }
2056
+
2057
+ const snapshots = JSON.parse(fs.readFileSync(snapshotFile, "utf-8"));
2058
+ const lookbackMs = (params.days ?? 30) * 24 * 60 * 60 * 1000;
2059
+ const cutoff = new Date(Date.now() - lookbackMs).toISOString();
2060
+
2061
+ const filtered = snapshots.filter((s: any) => s.timestamp >= cutoff);
2062
+
2063
+ if (filtered.length === 0) {
2064
+ return ok(JSON.stringify({
2065
+ status: "no_recent_history",
2066
+ message: `No snapshots in the last ${params.days ?? 30} days.`,
2067
+ totalSnapshots: snapshots.length,
2068
+ }));
2069
+ }
2070
+
2071
+ // Group by position and show trend
2072
+ const positions: Record<string, any[]> = {};
2073
+ for (const snap of filtered) {
2074
+ for (const pos of snap.positions) {
2075
+ if (params.token && !pos.collateralToken.includes(params.token)) continue;
2076
+ const key = `${pos.collateralToken}/${pos.loanToken}`;
2077
+ if (!positions[key]) positions[key] = [];
2078
+ positions[key].push({
2079
+ time: snap.timestamp,
2080
+ ltv: pos.ltv,
2081
+ hf: pos.healthFactor,
2082
+ collateral: pos.collateral,
2083
+ debt: pos.debt,
2084
+ });
2085
+ }
2086
+ }
2087
+
2088
+ const trends = Object.entries(positions).map(([pair, data]: [string, any[]]) => {
2089
+ const first = data[0];
2090
+ const last = data[data.length - 1];
2091
+ const ltvFirst = parseFloat(first.ltv);
2092
+ const ltvLast = parseFloat(last.ltv);
2093
+ const hfFirst = parseFloat(first.hf) || 0;
2094
+ const hfLast = parseFloat(last.hf) || 0;
2095
+
2096
+ return {
2097
+ pair,
2098
+ snapshots: data.length,
2099
+ firstSnapshot: first.time,
2100
+ lastSnapshot: last.time,
2101
+ ltvTrend: {
2102
+ start: first.ltv,
2103
+ current: last.ltv,
2104
+ change: ((ltvLast - ltvFirst)).toFixed(3) + '%',
2105
+ improving: ltvLast < ltvFirst,
2106
+ },
2107
+ hfTrend: {
2108
+ start: first.hf,
2109
+ current: last.hf,
2110
+ change: (hfLast - hfFirst).toFixed(3),
2111
+ improving: hfLast > hfFirst,
2112
+ },
2113
+ dataPoints: data,
2114
+ };
2115
+ });
2116
+
2117
+ return ok(JSON.stringify({
2118
+ status: "position_history",
2119
+ period: `Last ${params.days ?? 30} days`,
2120
+ totalSnapshots: filtered.length,
2121
+ trends,
2122
+ }));
2123
+ } catch (e) { return fail(e); }
2124
+ },
2125
+ });
2126
+
2127
+
1809
2128
  // ═══════════════════════════════════════════════════════
1810
2129
  // SLASH COMMANDS (no AI needed)
1811
2130
  // ═══════════════════════════════════════════════════════