@agether/agether 3.0.0 → 3.1.3

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