@agether/agether 2.0.0 → 2.0.1
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.
- package/README.md +83 -20
- package/openclaw.plugin.json +29 -22
- package/package.json +3 -3
- package/skills/agether/SKILL.md +104 -31
- package/src/index.ts +321 -18
- package/test/kya.test.ts +0 -157
- package/vitest.config.ts +0 -8
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @agether/agether
|
|
2
2
|
|
|
3
|
-
OpenClaw plugin for **Agether** —
|
|
3
|
+
OpenClaw plugin for **Agether** — onchain credit protocol for AI agents on Base.
|
|
4
4
|
|
|
5
5
|
## Prerequisites
|
|
6
6
|
|
|
@@ -14,9 +14,41 @@ OpenClaw plugin for **Agether** — on-chain credit protocol for AI agents on Ba
|
|
|
14
14
|
openclaw plugins install @agether/agether
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
## Configure
|
|
17
|
+
## Configure Secrets
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
The plugin reads sensitive credentials from **OpenClaw secrets** (environment variables), not from the plugin config. This keeps your private key out of `openclaw.json`.
|
|
20
|
+
|
|
21
|
+
### 1. Set your private key (required)
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
# Option A: Interactive setup (recommended)
|
|
25
|
+
openclaw secrets configure
|
|
26
|
+
# → source: env → id: AGETHER_PRIVATE_KEY
|
|
27
|
+
|
|
28
|
+
# Option B: Set directly in your shell / .env
|
|
29
|
+
export AGETHER_PRIVATE_KEY=0x...
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### 2. Set an RPC key (optional — better rate limits)
|
|
33
|
+
|
|
34
|
+
The plugin auto-detects RPC keys in this order, falling back to the free public endpoint:
|
|
35
|
+
|
|
36
|
+
| Env Var | Provider | URL pattern |
|
|
37
|
+
|---------|----------|------------|
|
|
38
|
+
| `ALCHEMY_API_KEY` | Alchemy | `https://base-mainnet.g.alchemy.com/v2/<key>` |
|
|
39
|
+
| `ANKR_API_KEY` | Ankr | `https://rpc.ankr.com/base/<key>` |
|
|
40
|
+
| `QUICKNODE_URL` | QuickNode | Used as-is (full URL) |
|
|
41
|
+
| *(none)* | PublicNode | `https://base-rpc.publicnode.com` (free, rate-limited) |
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Example: set via OpenClaw secrets
|
|
45
|
+
openclaw secrets configure
|
|
46
|
+
# → source: env → id: ALCHEMY_API_KEY
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 3. Plugin config (optional)
|
|
50
|
+
|
|
51
|
+
The plugin works with an **empty config**. All fields are optional:
|
|
20
52
|
|
|
21
53
|
```jsonc
|
|
22
54
|
// ~/.openclaw/openclaw.json
|
|
@@ -26,10 +58,13 @@ Edit `~/.openclaw/openclaw.json` and add the `agether` entry under `plugins.entr
|
|
|
26
58
|
"agether": {
|
|
27
59
|
"enabled": true,
|
|
28
60
|
"config": {
|
|
29
|
-
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
61
|
+
// All fields below are optional — plugin works with empty config {}
|
|
62
|
+
"agentId": "17676", // auto-saved after registration
|
|
63
|
+
"autoDraw": false, // auto-borrow for x402 payments
|
|
64
|
+
"dailySpendLimitUsdc": 0, // 0 = unlimited
|
|
65
|
+
"yieldLimitedSpending": false,
|
|
66
|
+
"autoDrawBuffer": 0,
|
|
67
|
+
"healthAlertThreshold": 70 // LTV % to trigger health warnings
|
|
33
68
|
}
|
|
34
69
|
}
|
|
35
70
|
}
|
|
@@ -43,7 +78,11 @@ Then restart the gateway:
|
|
|
43
78
|
openclaw gateway --force
|
|
44
79
|
```
|
|
45
80
|
|
|
46
|
-
|
|
81
|
+
### Verify secrets are clean
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
openclaw secrets audit --check
|
|
85
|
+
```
|
|
47
86
|
|
|
48
87
|
## Verify
|
|
49
88
|
|
|
@@ -59,18 +98,23 @@ Once installed, the following tools are available to your AI agent:
|
|
|
59
98
|
|
|
60
99
|
| Tool | Description |
|
|
61
100
|
|---|---|
|
|
101
|
+
| `agether_health` | **Start here** — comprehensive check: balances, positions, LTV, liquidation risk, headroom |
|
|
102
|
+
| `agether_preflight` | Setup readiness checklist: key, RPC, registration, balances |
|
|
62
103
|
| `agether_balance` | Check ETH & USDC balances (EOA + AgentAccount) |
|
|
63
104
|
| `agether_register` | Mint ERC-8004 identity & create AgentAccount |
|
|
64
105
|
| `agether_set_agent` | Set a known agentId (from memory) and save to config |
|
|
65
106
|
| `agether_score` | Get Bayesian credit score with 5-factor breakdown |
|
|
66
107
|
| `morpho_status` | Show all Morpho credit positions |
|
|
67
108
|
| `morpho_markets` | List supported Morpho Blue markets |
|
|
109
|
+
| `morpho_rates` | Current market rates — supply APY, borrow APY, utilization |
|
|
110
|
+
| `morpho_max_borrowable` | Calculate max additional USDC borrowable |
|
|
111
|
+
| `morpho_yield_estimate` | Estimate theoretical yield for collateral |
|
|
68
112
|
| `morpho_deposit` | Deposit collateral (WETH, wstETH, cbETH) |
|
|
69
|
-
| `morpho_deposit_and_borrow` | Deposit collateral + borrow USDC in one
|
|
113
|
+
| `morpho_deposit_and_borrow` | Deposit collateral + borrow USDC in one batched tx |
|
|
70
114
|
| `morpho_borrow` | Borrow USDC against deposited collateral |
|
|
71
115
|
| `morpho_repay` | Repay USDC debt |
|
|
72
116
|
| `morpho_withdraw` | Withdraw collateral back to EOA |
|
|
73
|
-
| `morpho_sponsor` |
|
|
117
|
+
| `morpho_sponsor` | Deposit collateral for another agent (by ID or address) |
|
|
74
118
|
| `wallet_fund` | Transfer USDC from EOA into AgentAccount |
|
|
75
119
|
| `x402_pay` | Make paid API calls via x402 protocol (supports auto-draw) |
|
|
76
120
|
|
|
@@ -80,16 +124,25 @@ Quick commands that run without AI reasoning:
|
|
|
80
124
|
|
|
81
125
|
- `/balance` — wallet balances
|
|
82
126
|
- `/morpho` — Morpho positions overview
|
|
127
|
+
- `/health` — position health: LTV, liquidation risk, balances
|
|
128
|
+
- `/rates` — current Morpho market rates
|
|
129
|
+
|
|
130
|
+
## Hooks
|
|
131
|
+
|
|
132
|
+
The plugin registers automatic hooks:
|
|
133
|
+
|
|
134
|
+
- **`command:new`** — logs agent balance context at the start of each new conversation
|
|
83
135
|
|
|
84
136
|
## Quick Start
|
|
85
137
|
|
|
86
138
|
1. Install: `openclaw plugins install @agether/agether`
|
|
87
|
-
2.
|
|
88
|
-
3.
|
|
89
|
-
4.
|
|
90
|
-
5.
|
|
91
|
-
6.
|
|
92
|
-
7.
|
|
139
|
+
2. Set secret: `export AGETHER_PRIVATE_KEY=0x...` (or use `openclaw secrets configure`)
|
|
140
|
+
3. Enable: add `"agether": { "enabled": true, "config": {} }` to `plugins.entries`
|
|
141
|
+
4. Restart: `openclaw gateway --force`
|
|
142
|
+
5. Open Telegram → *"What is my balance?"*
|
|
143
|
+
6. *"Register me as an agent on Agether"*
|
|
144
|
+
7. Fund your wallet with ETH (gas) + WETH/wstETH/cbETH (collateral)
|
|
145
|
+
8. *"Deposit 0.05 WETH and borrow $50 USDC"*
|
|
93
146
|
|
|
94
147
|
## Getting Collateral on Base
|
|
95
148
|
|
|
@@ -101,11 +154,21 @@ Quick commands that run without AI reasoning:
|
|
|
101
154
|
|
|
102
155
|
| Option | Required | Default | Description |
|
|
103
156
|
|---|---|---|---|
|
|
104
|
-
| `privateKey` | ✅ | — | Wallet private key for signing transactions |
|
|
105
|
-
| `rpcUrl` | — | `https://base-rpc.publicnode.com` | Base RPC endpoint |
|
|
106
|
-
| `backendUrl` | — | `http://95.179.189.214:3001` | Agether backend API |
|
|
107
|
-
| `autoDraw` | — | `false` | Auto-borrow from credit line when USDC is insufficient for x402 payments |
|
|
108
157
|
| `agentId` | — | — | Pre-set agent ID (auto-saved after registration) |
|
|
158
|
+
| `autoDraw` | — | `false` | Auto-borrow from credit line when USDC is insufficient for x402 payments |
|
|
159
|
+
| `dailySpendLimitUsdc` | — | `0` | Daily USDC spending cap (0 = unlimited) |
|
|
160
|
+
| `yieldLimitedSpending` | — | `false` | Limit auto-draw to theoretical yield |
|
|
161
|
+
| `autoDrawBuffer` | — | `0` | Extra USDC buffer when auto-drawing |
|
|
162
|
+
| `healthAlertThreshold` | — | `70` | LTV % to trigger health warnings |
|
|
163
|
+
|
|
164
|
+
### Secrets (env vars)
|
|
165
|
+
|
|
166
|
+
| Env Var | Required | Description |
|
|
167
|
+
|---|---|---|
|
|
168
|
+
| `AGETHER_PRIVATE_KEY` | ✅ | Wallet private key for signing transactions |
|
|
169
|
+
| `ALCHEMY_API_KEY` | — | Alchemy API key for premium Base RPC |
|
|
170
|
+
| `ANKR_API_KEY` | — | Ankr API key for premium Base RPC |
|
|
171
|
+
| `QUICKNODE_URL` | — | Full QuickNode RPC URL |
|
|
109
172
|
|
|
110
173
|
## Links
|
|
111
174
|
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,44 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "agether",
|
|
3
3
|
"name": "Agether Credit",
|
|
4
|
-
"description": "
|
|
5
|
-
"version": "
|
|
4
|
+
"description": "Onchain credit protocol for AI agents — Morpho-backed overcollateralized credit, ERC-8004 identity, x402 payments. Private key and RPC keys are read from OpenClaw secrets (env vars); no plaintext config needed.",
|
|
5
|
+
"version": "2.0.1",
|
|
6
6
|
"skills": ["skills/agether"],
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
|
9
9
|
"additionalProperties": false,
|
|
10
10
|
"properties": {
|
|
11
|
-
"privateKey": {
|
|
12
|
-
"type": "string",
|
|
13
|
-
"description": "Wallet private key for signing transactions"
|
|
14
|
-
},
|
|
15
11
|
"agentId": {
|
|
16
12
|
"type": "string",
|
|
17
|
-
"description": "ERC-8004 agent ID (set after registration)"
|
|
18
|
-
},
|
|
19
|
-
"rpcUrl": {
|
|
20
|
-
"type": "string",
|
|
21
|
-
"description": "Base RPC endpoint",
|
|
22
|
-
"default": "https://base-rpc.publicnode.com"
|
|
23
|
-
},
|
|
24
|
-
"backendUrl": {
|
|
25
|
-
"type": "string",
|
|
26
|
-
"description": "Agether backend URL",
|
|
27
|
-
"default": "http://95.179.189.214:3001"
|
|
13
|
+
"description": "ERC-8004 agent ID (set after registration, auto-saved by tools)"
|
|
28
14
|
},
|
|
29
15
|
"autoDraw": {
|
|
30
16
|
"type": "boolean",
|
|
31
17
|
"description": "Auto-borrow from Morpho credit line when USDC balance is low for x402 payments",
|
|
32
18
|
"default": false
|
|
19
|
+
},
|
|
20
|
+
"dailySpendLimitUsdc": {
|
|
21
|
+
"type": "number",
|
|
22
|
+
"description": "Daily USDC spending cap for x402 payments (0 = unlimited)",
|
|
23
|
+
"default": 0
|
|
24
|
+
},
|
|
25
|
+
"yieldLimitedSpending": {
|
|
26
|
+
"type": "boolean",
|
|
27
|
+
"description": "Limit auto-draw spending to theoretical yield on deposited collateral",
|
|
28
|
+
"default": false
|
|
29
|
+
},
|
|
30
|
+
"autoDrawBuffer": {
|
|
31
|
+
"type": "number",
|
|
32
|
+
"description": "Extra USDC buffer when auto-drawing from Morpho (e.g. 0.5 = borrow $0.50 extra)",
|
|
33
|
+
"default": 0
|
|
34
|
+
},
|
|
35
|
+
"healthAlertThreshold": {
|
|
36
|
+
"type": "number",
|
|
37
|
+
"description": "LTV percentage threshold for health warnings (e.g. 70 = warn at 70% LTV, liquidation at 80%)",
|
|
38
|
+
"default": 70
|
|
33
39
|
}
|
|
34
40
|
},
|
|
35
|
-
"required": [
|
|
41
|
+
"required": []
|
|
36
42
|
},
|
|
37
43
|
"uiHints": {
|
|
38
|
-
"privateKey": { "label": "Private Key", "sensitive": true },
|
|
39
44
|
"agentId": { "label": "Agent ID", "placeholder": "17676" },
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
45
|
+
"autoDraw": { "label": "Auto-Draw Credit", "placeholder": "false" },
|
|
46
|
+
"dailySpendLimitUsdc": { "label": "Daily Spend Limit (USDC)", "placeholder": "0" },
|
|
47
|
+
"yieldLimitedSpending": { "label": "Yield-Limited Spending", "placeholder": "false" },
|
|
48
|
+
"autoDrawBuffer": { "label": "Auto-Draw Buffer (USDC)", "placeholder": "0" },
|
|
49
|
+
"healthAlertThreshold": { "label": "Health Alert Threshold (%)", "placeholder": "70" }
|
|
43
50
|
}
|
|
44
51
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agether/agether",
|
|
3
|
-
"version": "2.0.
|
|
4
|
-
"description": "OpenClaw plugin for Agether —
|
|
3
|
+
"version": "2.0.1",
|
|
4
|
+
"description": "OpenClaw plugin for Agether — onchain credit for AI agents",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"openclaw": {
|
|
7
7
|
"extensions": [
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
]
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@agether/sdk": "^2.
|
|
12
|
+
"@agether/sdk": "^2.3.4",
|
|
13
13
|
"axios": "^1.6.0",
|
|
14
14
|
"ethers": "^6.9.0"
|
|
15
15
|
},
|
package/skills/agether/SKILL.md
CHANGED
|
@@ -26,21 +26,23 @@ If you don't paste it, the user sees NOTHING. An empty colon ":" with no data af
|
|
|
26
26
|
1. **PASTE tool results into your reply.** Every tool returns JSON. Extract the key fields and write them in your message. The user cannot see tool output directly.
|
|
27
27
|
2. **ALWAYS paste tx links.** Tools return `tx` field like `https://basescan.org/tx/0x...`. Copy it verbatim into your reply.
|
|
28
28
|
3. **After on-chain actions, call `agether_balance` and paste the balances.**
|
|
29
|
-
4. **On first message, call `
|
|
29
|
+
4. **On first message, call `agether_health`.** This is your single best "context loader" — it returns balances, positions, LTV, alerts, and headroom in one call. If `agentId` is `"?"`, see AGENT ID RESOLUTION below.
|
|
30
30
|
5. **Be proactive** — if the user asks to call a paid API, do the full flow without asking.
|
|
31
|
-
6. **Never ask for private keys** —
|
|
31
|
+
6. **Never ask for private keys** — they come from OpenClaw secrets (AGETHER_PRIVATE_KEY env var).
|
|
32
32
|
7. **Max LTV is 80%** (125% collateral ratio). To borrow $X, you need $X × 1.25 in collateral value.
|
|
33
33
|
8. **When user says "register" → ALWAYS call `agether_register`.** A wallet CAN have multiple ERC-8004 identities. The tool handles everything. Never refuse to register because the wallet "already has an identity". Just call the tool.
|
|
34
34
|
9. **Never guess about blockchain state.** If you're unsure, call the tool. Don't tell the user something is impossible — try it first.
|
|
35
35
|
10. **Your EOA wallet IS the user's wallet.** Same private key, same address. If the user says "I have WETH", call `agether_balance` — you'll see it in the `collateral` field. You already have access to those tokens.
|
|
36
36
|
11. **`morpho_deposit` and `morpho_deposit_and_borrow` transfer collateral from your EOA automatically.** You do NOT need anyone to "send you" tokens. If your EOA has WETH, just call the deposit tool directly. The SDK handles EOA→Safe account→Morpho in one flow.
|
|
37
37
|
12. **To get USDC for x402:** call `morpho_deposit_and_borrow` with collateral from your EOA. Do NOT ask the user to send tokens — check your balance first.
|
|
38
|
+
13. **Check health before large actions.** Before borrowing, depositing, or withdrawing significant amounts, call `agether_health` to see current LTV and headroom. This prevents failed transactions.
|
|
39
|
+
14. **Batch awareness:** `morpho_deposit_and_borrow` is a batched operation (deposit + borrow in one tx). Always prefer it over separate `morpho_deposit` then `morpho_borrow` when doing both — it saves gas and is atomic.
|
|
38
40
|
|
|
39
41
|
---
|
|
40
42
|
|
|
41
43
|
## 🆔 AGENT ID RESOLUTION (CRITICAL)
|
|
42
44
|
|
|
43
|
-
If `agether_balance` returns `agentId: "?"`, ALL Morpho tools will fail. You MUST resolve this first:
|
|
45
|
+
If `agether_health` or `agether_balance` returns `agentId: "?"`, ALL Morpho tools will fail. You MUST resolve this first:
|
|
44
46
|
|
|
45
47
|
```
|
|
46
48
|
1. Check your memory for a previously registered agentId
|
|
@@ -50,7 +52,7 @@ If `agether_balance` returns `agentId: "?"`, ALL Morpho tools will fail. You MUS
|
|
|
50
52
|
→ IF user says no → call `agether_register(name: "<ask name>")`
|
|
51
53
|
3. IF no agentId in memory:
|
|
52
54
|
→ call `agether_register(name: "<ask name>")`
|
|
53
|
-
4. After either path → call `
|
|
55
|
+
4. After either path → call `agether_health` to confirm everything is working
|
|
54
56
|
```
|
|
55
57
|
|
|
56
58
|
Both `agether_set_agent` and `agether_register` save the agentId to config permanently (survives restarts).
|
|
@@ -59,6 +61,12 @@ Both `agether_set_agent` and `agether_register` save the agentId to config perma
|
|
|
59
61
|
|
|
60
62
|
## 🛠 YOUR TOOLS
|
|
61
63
|
|
|
64
|
+
### Health & Diagnostics
|
|
65
|
+
| Tool | What it does |
|
|
66
|
+
|------|-------------|
|
|
67
|
+
| `agether_health` | **START HERE.** Comprehensive check: balances, all positions with LTV & liquidation risk, borrowing headroom, alerts — all in one call. |
|
|
68
|
+
| `agether_preflight` | Setup readiness check: private key present? RPC working? Agent registered? Balances OK? Returns pass/fail checklist. |
|
|
69
|
+
|
|
62
70
|
### Identity & Wallet
|
|
63
71
|
| Tool | What it does |
|
|
64
72
|
|------|-------------|
|
|
@@ -73,70 +81,118 @@ Both `agether_set_agent` and `agether_register` save the agentId to config perma
|
|
|
73
81
|
|------|--------|-------------|
|
|
74
82
|
| `morpho_markets` | none | List supported Morpho markets and parameters |
|
|
75
83
|
| `morpho_status` | none | Show all Morpho positions (collateral + debt) |
|
|
84
|
+
| `morpho_rates` | `token?` | Current market rates — supply APY, borrow APY, utilization |
|
|
85
|
+
| `morpho_max_borrowable` | none | Calculate max additional USDC borrowable given current collateral/debt |
|
|
86
|
+
| `morpho_yield_estimate` | `token`, `amount`, `periodDays?` | Estimate theoretical yield for collateral |
|
|
76
87
|
| `morpho_deposit` | `amount`, `token` | Deposit collateral from EOA → Morpho (no borrow) |
|
|
77
|
-
| `morpho_deposit_and_borrow` | `collateralAmount`, `token`, `borrowAmount` | Deposit + borrow in one
|
|
88
|
+
| `morpho_deposit_and_borrow` | `collateralAmount`, `token`, `borrowAmount` | **PREFERRED** — Deposit + borrow in one batched tx. Best for first-time setup |
|
|
78
89
|
| `morpho_borrow` | `amount` (USD) | Borrow USDC against existing collateral → lands in Safe account |
|
|
79
|
-
| `morpho_repay` | `amount` (USD) | Repay USDC debt from Safe account → Morpho |
|
|
90
|
+
| `morpho_repay` | `amount` (USD or "all") | Repay USDC debt from Safe account → Morpho |
|
|
80
91
|
| `morpho_withdraw` | `amount` (or "all"), `token` | Withdraw collateral back to EOA wallet |
|
|
81
|
-
| `morpho_sponsor` | `agentId`/`agentAddress`, `amount`, `token
|
|
92
|
+
| `morpho_sponsor` | `agentId`/`agentAddress`, `amount`, `token` | Deposit collateral for another agent |
|
|
82
93
|
|
|
83
94
|
### x402 Payments
|
|
84
95
|
| Tool | Params | What it does |
|
|
85
96
|
|------|--------|-------------|
|
|
86
97
|
| `x402_pay` | `url`, `method?`, `body?`, `autoDraw?` | Make a paid API call. Pays with USDC via EIP-3009 signature |
|
|
87
98
|
|
|
88
|
-
### Slash Commands (user types these)
|
|
99
|
+
### Slash Commands (user types these — no AI invocation)
|
|
89
100
|
| Command | What it does |
|
|
90
101
|
|---------|-------------|
|
|
91
102
|
| `/balance` | Quick balance check |
|
|
92
103
|
| `/morpho` | Quick Morpho position summary |
|
|
104
|
+
| `/health` | Quick position health: LTV, liquidation risk, balances |
|
|
105
|
+
| `/rates` | Current Morpho market rates |
|
|
93
106
|
|
|
94
107
|
---
|
|
95
108
|
|
|
96
109
|
## 📋 DECISION TREES
|
|
97
110
|
|
|
111
|
+
### Session start (first message or new conversation)
|
|
112
|
+
```
|
|
113
|
+
1. agether_health ← ONE call for full context
|
|
114
|
+
2. Check the "alerts" array:
|
|
115
|
+
→ If any 🔴 alerts → WARN user immediately (liquidation risk!)
|
|
116
|
+
→ If any 🟡 alerts → mention the risk casually
|
|
117
|
+
→ If "agentId: ?" → go to AGENT ID RESOLUTION
|
|
118
|
+
3. Now you have full context to handle any request
|
|
119
|
+
```
|
|
120
|
+
|
|
98
121
|
### User asks to call a paid API (x402)
|
|
99
122
|
```
|
|
100
|
-
1.
|
|
101
|
-
2.
|
|
123
|
+
1. agether_health ← get full picture in one call
|
|
124
|
+
2. Look at balances.agentAccount.usdc:
|
|
125
|
+
IF USDC ≥ $0.01:
|
|
102
126
|
→ x402_pay(url)
|
|
103
|
-
3. IF no USDC but
|
|
127
|
+
3. IF no USDC but positions exist with headroom:
|
|
104
128
|
→ morpho_borrow(amount: "1") ← borrow $1
|
|
105
129
|
→ x402_pay(url)
|
|
106
|
-
4. IF no USDC AND no
|
|
130
|
+
4. IF no USDC AND no positions BUT EOA has WETH/wstETH/cbETH:
|
|
107
131
|
→ morpho_deposit_and_borrow(collateralAmount, token, borrowAmount: "1")
|
|
108
132
|
→ x402_pay(url)
|
|
109
|
-
5. IF
|
|
110
|
-
→ Tell user: "Need collateral (WETH/wstETH/cbETH) on Base
|
|
133
|
+
5. IF nothing at all:
|
|
134
|
+
→ Tell user: "Need collateral (WETH/wstETH/cbETH) on Base. Send to <EOA address>"
|
|
111
135
|
```
|
|
112
136
|
|
|
113
137
|
### User wants to borrow USDC via Morpho
|
|
114
138
|
```
|
|
115
|
-
1.
|
|
116
|
-
2.
|
|
117
|
-
|
|
118
|
-
|
|
139
|
+
1. agether_health ← check collateral, headroom, current LTV
|
|
140
|
+
2. IF headroom allows the borrow amount AND collateral already in Morpho:
|
|
141
|
+
→ morpho_borrow(amount)
|
|
142
|
+
3. IF EOA has collateral but nothing in Morpho yet:
|
|
143
|
+
→ morpho_deposit_and_borrow(collateralAmount, token, borrowAmount)
|
|
144
|
+
4. agether_balance ← show the result with balance changes
|
|
145
|
+
5. Show: tx hash, collateral deposited, USDC borrowed, new balances, new LTV
|
|
119
146
|
```
|
|
120
147
|
|
|
121
148
|
### User wants to repay + withdraw
|
|
122
149
|
```
|
|
123
|
-
1.
|
|
124
|
-
2. morpho_repay(amount)
|
|
150
|
+
1. agether_health ← check current debt, collateral, LTV
|
|
151
|
+
2. morpho_repay(amount or "all") ← repay USDC
|
|
125
152
|
3. morpho_withdraw("all", token) ← get collateral back
|
|
126
153
|
4. agether_balance ← show final balances
|
|
127
154
|
```
|
|
128
155
|
|
|
156
|
+
### User asks about position health or liquidation risk
|
|
157
|
+
```
|
|
158
|
+
1. agether_health ← returns LTV, risk level, alerts per position
|
|
159
|
+
2. Paste the positions table with LTV percentages
|
|
160
|
+
3. IF any position has LTV > 70%:
|
|
161
|
+
→ Suggest: "Consider repaying debt or adding collateral"
|
|
162
|
+
→ Show: morpho_max_borrowable for headroom context
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### User wants to optimize rates or compare markets
|
|
166
|
+
```
|
|
167
|
+
1. morpho_rates ← get all market rates
|
|
168
|
+
2. morpho_yield_estimate(token, amount) ← estimate yield
|
|
169
|
+
3. Present: which market has lowest borrow APY, highest supply APY
|
|
170
|
+
4. Suggest optimal strategy based on their holdings
|
|
171
|
+
```
|
|
172
|
+
|
|
129
173
|
### User asks to check or refresh credit score
|
|
130
174
|
```
|
|
131
175
|
1. IF user says "refresh" / "update" / "get new score":
|
|
132
|
-
→
|
|
176
|
+
→ Check USDC balance first (refresh costs x402)
|
|
177
|
+
→ agether_score(refresh: true) ← pays via x402, computes fresh score
|
|
133
178
|
→ Paste the score and tx link
|
|
134
179
|
2. IF user says "check" / "what's my score":
|
|
135
|
-
→ agether_score(refresh: false) ← free
|
|
180
|
+
→ agether_score(refresh: false) ← free read
|
|
136
181
|
→ Paste the score
|
|
137
182
|
3. NEVER say "not registered" or "no score yet" without calling the tool first!
|
|
138
183
|
```
|
|
139
184
|
|
|
185
|
+
### Something seems wrong / setup issues
|
|
186
|
+
```
|
|
187
|
+
1. agether_preflight ← run the diagnostic checklist
|
|
188
|
+
2. Show each check with its status
|
|
189
|
+
3. Address any ❌ or ⚠️ items:
|
|
190
|
+
- Missing private key → guide to `openclaw secrets configure`
|
|
191
|
+
- Not registered → offer to register
|
|
192
|
+
- No ETH for gas → show EOA address for funding
|
|
193
|
+
- No USDC → suggest deposit_and_borrow flow
|
|
194
|
+
```
|
|
195
|
+
|
|
140
196
|
### User asks "what can I do?" or "what is agether?"
|
|
141
197
|
```
|
|
142
198
|
Explain: You're an AI agent with on-chain credit on Base.
|
|
@@ -144,6 +200,7 @@ You can:
|
|
|
144
200
|
- Deposit WETH/wstETH/cbETH as collateral
|
|
145
201
|
- Borrow USDC instantly (125% collateral ratio)
|
|
146
202
|
- Pay for APIs using x402 protocol
|
|
203
|
+
- Monitor position health and avoid liquidation
|
|
147
204
|
- All on Base mainnet, all autonomous
|
|
148
205
|
```
|
|
149
206
|
|
|
@@ -157,7 +214,7 @@ Example after repay:
|
|
|
157
214
|
```
|
|
158
215
|
✅ Repaid $1.50 USDC
|
|
159
216
|
|
|
160
|
-
|
|
217
|
+
🔗 Tx: https://basescan.org/tx/0xabc123def456...
|
|
161
218
|
💰 Balances:
|
|
162
219
|
EOA: 0.05 ETH, 0 USDC
|
|
163
220
|
AgentAccount: 0 ETH, $3.50 USDC
|
|
@@ -199,6 +256,7 @@ If something **fails**:
|
|
|
199
256
|
|
|
200
257
|
| Error | Meaning | What to do |
|
|
201
258
|
|-------|---------|-----------|
|
|
259
|
+
| `Missing AGETHER_PRIVATE_KEY` | Private key not set in secrets | Run `openclaw secrets configure` → source: env → id: AGETHER_PRIVATE_KEY |
|
|
202
260
|
| `ExceedsMaxLtv` | Collateral too low for borrow amount | Deposit more collateral or borrow less. LTV must be ≤ 80% |
|
|
203
261
|
| `Payment rejected (402)` | No USDC for x402 payment | Borrow USDC first: `morpho_borrow` or `morpho_deposit_and_borrow` |
|
|
204
262
|
| `No collateral deposited` | Trying to borrow without collateral | `morpho_deposit` first |
|
|
@@ -208,10 +266,25 @@ If something **fails**:
|
|
|
208
266
|
|
|
209
267
|
---
|
|
210
268
|
|
|
269
|
+
## 🔐 SECRETS SETUP (for reference)
|
|
270
|
+
|
|
271
|
+
The plugin reads secrets from environment variables via OpenClaw's secrets system:
|
|
272
|
+
|
|
273
|
+
| Secret | Env Var | Required | Notes |
|
|
274
|
+
|--------|---------|----------|-------|
|
|
275
|
+
| Wallet private key | `AGETHER_PRIVATE_KEY` | Yes | Set via `openclaw secrets configure` → source: env |
|
|
276
|
+
| Alchemy RPC key | `ALCHEMY_API_KEY` | No | Higher rate limits than free RPC |
|
|
277
|
+
| Ankr RPC key | `ANKR_API_KEY` | No | Alternative premium RPC |
|
|
278
|
+
| QuickNode RPC URL | `QUICKNODE_URL` | No | Full URL including API key |
|
|
279
|
+
|
|
280
|
+
If no RPC key is set, the plugin falls back to `https://base-rpc.publicnode.com` (free, rate-limited).
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
211
284
|
## 🏗 ARCHITECTURE (for context)
|
|
212
285
|
|
|
213
286
|
```
|
|
214
|
-
EOA Wallet (private key)
|
|
287
|
+
EOA Wallet (private key from AGETHER_PRIVATE_KEY)
|
|
215
288
|
├── Owns ERC-8004 Identity NFT (agentId) ← can own MULTIPLE
|
|
216
289
|
└── Owns Safe Account (via Safe7579 + ERC-4337, 1 per agentId)
|
|
217
290
|
├── Holds USDC (from borrows)
|
|
@@ -231,13 +304,13 @@ Morpho Blue (direct lending, overcollateralized 125%)
|
|
|
231
304
|
|
|
232
305
|
| Contract | Address |
|
|
233
306
|
|----------|---------|
|
|
234
|
-
|
|
|
235
|
-
|
|
|
236
|
-
|
|
|
237
|
-
|
|
|
238
|
-
| ValidationRegistry |
|
|
239
|
-
|
|
|
240
|
-
| TimelockController | `
|
|
307
|
+
| Agether4337Factory | `0x67DE66D07ff3dba0092C21f070f8a0f53D657BC3` |
|
|
308
|
+
| Agether8004ValidationModule | `0xde896C58163b5f6cAC5B16C1b0109843f26106F6` |
|
|
309
|
+
| AgetherHookMultiplexer | `0x4AB6DaD0f7360fa8d8c75889A5c206B65d7CbeDb` |
|
|
310
|
+
| Agether7579Bootstrap | `0xCc83AA714c05B7141B21a17e80EB21bD09652b27` |
|
|
311
|
+
| ValidationRegistry | Not deployed yet |
|
|
312
|
+
| Agether8004Scorer | `0x56c7D35A976fac67b1993b66b861fCA32f59104F` |
|
|
313
|
+
| TimelockController | `0xc600e7AAB8a230326C714CE66f356fdf6aC021d8` |
|
|
241
314
|
| Safe Singleton | `0x41675C099F32341bf84BFc5382aF534df5C7461a` |
|
|
242
315
|
| SafeProxyFactory | `0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67` |
|
|
243
316
|
| Safe7579 | `0x7579EE8307284F293B1927136486880611F20002` |
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Each tool = MorphoClient / X402Client call → format result with txLink.
|
|
5
5
|
* Market discovery via Morpho GraphQL API (no backend dependency for markets).
|
|
6
|
+
*
|
|
7
|
+
* v2: Secrets-first config
|
|
8
|
+
* ─────────────────────────
|
|
9
|
+
* - privateKey → AGETHER_PRIVATE_KEY env var (set via `openclaw secrets configure` → crypto)
|
|
10
|
+
* - rpcUrl → auto-resolved from ALCHEMY_API_KEY / ANKR_API_KEY / QUICKNODE_URL env vars,
|
|
11
|
+
* falls back to https://base-rpc.publicnode.com
|
|
12
|
+
* - backendUrl → hardcoded (no config needed)
|
|
13
|
+
* - agentId → optional plugin config field (auto-saved by tools)
|
|
14
|
+
*
|
|
15
|
+
* The plugin works with an empty `plugins.entries.agether.config: {}`.
|
|
6
16
|
*/
|
|
7
17
|
|
|
8
18
|
import axios from "axios";
|
|
@@ -13,6 +23,12 @@ import {
|
|
|
13
23
|
X402Client,
|
|
14
24
|
} from "@agether/sdk";
|
|
15
25
|
|
|
26
|
+
// ─── Constants ────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
const BACKEND_URL = "http://95.179.189.214:3001";
|
|
29
|
+
const DEFAULT_RPC = "https://base-rpc.publicnode.com";
|
|
30
|
+
const BASESCAN = "https://basescan.org/tx";
|
|
31
|
+
|
|
16
32
|
// ─── Helpers ──────────────────────────────────────────────
|
|
17
33
|
|
|
18
34
|
interface PluginConfig {
|
|
@@ -22,7 +38,6 @@ interface PluginConfig {
|
|
|
22
38
|
backendUrl: string;
|
|
23
39
|
}
|
|
24
40
|
|
|
25
|
-
const BASESCAN = "https://basescan.org/tx";
|
|
26
41
|
function txLink(hash: string): string {
|
|
27
42
|
return hash ? `${BASESCAN}/${hash}` : "";
|
|
28
43
|
}
|
|
@@ -44,16 +59,48 @@ function fail(err: unknown) {
|
|
|
44
59
|
return { content: [{ type: "text" as const, text: `❌ ${msg}` }], isError: true };
|
|
45
60
|
}
|
|
46
61
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
62
|
+
// ─── Secrets-first config resolution ──────────────────────
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolve RPC URL from environment secrets.
|
|
66
|
+
* Priority: ALCHEMY_API_KEY → ANKR_API_KEY → QUICKNODE_URL → publicnode fallback.
|
|
67
|
+
*/
|
|
68
|
+
function resolveRpcUrl(): string {
|
|
69
|
+
const alchemy = process.env.ALCHEMY_API_KEY;
|
|
70
|
+
if (alchemy) return `https://base-mainnet.g.alchemy.com/v2/${alchemy}`;
|
|
71
|
+
|
|
72
|
+
const ankr = process.env.ANKR_API_KEY;
|
|
73
|
+
if (ankr) return `https://rpc.ankr.com/base/${ankr}`;
|
|
74
|
+
|
|
75
|
+
const quicknode = process.env.QUICKNODE_URL;
|
|
76
|
+
if (quicknode) return quicknode;
|
|
77
|
+
|
|
78
|
+
return DEFAULT_RPC;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Resolve private key from environment secrets.
|
|
83
|
+
* Set via `openclaw secrets configure` (source: env → AGETHER_PRIVATE_KEY).
|
|
84
|
+
*/
|
|
85
|
+
function resolvePrivateKey(): string {
|
|
86
|
+
const key = process.env.AGETHER_PRIVATE_KEY;
|
|
87
|
+
if (!key) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
"Missing AGETHER_PRIVATE_KEY. " +
|
|
90
|
+
"Set it via: openclaw secrets configure → source: env → id: AGETHER_PRIVATE_KEY\n" +
|
|
91
|
+
"Or export AGETHER_PRIVATE_KEY=0x... in your shell / .env file.",
|
|
92
|
+
);
|
|
51
93
|
}
|
|
94
|
+
return key;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getConfig(api: any): PluginConfig {
|
|
98
|
+
const cfg = api.config?.plugins?.entries?.["agether"]?.config ?? {};
|
|
52
99
|
return {
|
|
53
|
-
privateKey:
|
|
100
|
+
privateKey: resolvePrivateKey(),
|
|
54
101
|
agentId: cfg.agentId,
|
|
55
|
-
rpcUrl:
|
|
56
|
-
backendUrl:
|
|
102
|
+
rpcUrl: resolveRpcUrl(),
|
|
103
|
+
backendUrl: BACKEND_URL,
|
|
57
104
|
};
|
|
58
105
|
}
|
|
59
106
|
|
|
@@ -76,13 +123,17 @@ function persistAgentId(agentId: string): string {
|
|
|
76
123
|
const home = process.env.HOME || process.env.USERPROFILE || "/root";
|
|
77
124
|
const cfgPath = path.join(home, ".openclaw", "openclaw.json");
|
|
78
125
|
const raw = fs.readFileSync(cfgPath, "utf-8");
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
126
|
+
const json = JSON.parse(raw);
|
|
127
|
+
|
|
128
|
+
// Ensure config path exists
|
|
129
|
+
if (!json.plugins) json.plugins = {};
|
|
130
|
+
if (!json.plugins.entries) json.plugins.entries = {};
|
|
131
|
+
if (!json.plugins.entries.agether) json.plugins.entries.agether = {};
|
|
132
|
+
if (!json.plugins.entries.agether.config) json.plugins.entries.agether.config = {};
|
|
133
|
+
|
|
134
|
+
json.plugins.entries.agether.config.agentId = agentId;
|
|
135
|
+
fs.writeFileSync(cfgPath, JSON.stringify(json, null, 2));
|
|
136
|
+
return "saved";
|
|
86
137
|
} catch (e) {
|
|
87
138
|
return `write failed: ${e instanceof Error ? e.message : String(e)}`;
|
|
88
139
|
}
|
|
@@ -164,7 +215,7 @@ export default function register(api: any) {
|
|
|
164
215
|
async execute(_id: string, params: { agentId: string }) {
|
|
165
216
|
try {
|
|
166
217
|
const cfg = getConfig(api);
|
|
167
|
-
// Verify
|
|
218
|
+
// Verify onchain that this agentId exists and belongs to this wallet
|
|
168
219
|
const client = new MorphoClient({
|
|
169
220
|
privateKey: cfg.privateKey,
|
|
170
221
|
rpcUrl: cfg.rpcUrl,
|
|
@@ -522,7 +573,7 @@ export default function register(api: any) {
|
|
|
522
573
|
api.registerTool({
|
|
523
574
|
name: "agether_score",
|
|
524
575
|
description:
|
|
525
|
-
"Get the agent's current
|
|
576
|
+
"Get the agent's current onchain credit score. " +
|
|
526
577
|
"Use 'refresh' param to request a fresh score computation (costs USDC via x402).",
|
|
527
578
|
parameters: {
|
|
528
579
|
type: "object",
|
|
@@ -546,13 +597,15 @@ export default function register(api: any) {
|
|
|
546
597
|
backendUrl: cfg.backendUrl,
|
|
547
598
|
agentId,
|
|
548
599
|
accountAddress,
|
|
600
|
+
// Safe7579 needs validator prefix for ERC-1271 isValidSignature routing
|
|
601
|
+
validatorModule: "0xde896C58163b5f6cAC5B16C1b0109843f26106F6",
|
|
549
602
|
});
|
|
550
603
|
const result = await x402.get(`${cfg.backendUrl}/score/${agentId}`);
|
|
551
604
|
if (!result.success) return fail(result.error || "Score request failed");
|
|
552
605
|
return ok(JSON.stringify(result.data, null, 2));
|
|
553
606
|
}
|
|
554
607
|
|
|
555
|
-
// Free: read current
|
|
608
|
+
// Free: read current onchain score
|
|
556
609
|
const { data } = await axios.get(`${cfg.backendUrl}/score/${agentId}/current`);
|
|
557
610
|
return ok(JSON.stringify(data, null, 2));
|
|
558
611
|
} catch (e) { return fail(e); }
|
|
@@ -631,6 +684,8 @@ export default function register(api: any) {
|
|
|
631
684
|
dailySpendLimitUsdc: agetherCfg.dailySpendLimitUsdc,
|
|
632
685
|
yieldLimitedSpending: agetherCfg.yieldLimitedSpending,
|
|
633
686
|
autoDrawBuffer: agetherCfg.autoDrawBuffer,
|
|
687
|
+
// Safe7579 needs validator prefix for ERC-1271 isValidSignature routing
|
|
688
|
+
validatorModule: "0xde896C58163b5f6cAC5B16C1b0109843f26106F6",
|
|
634
689
|
});
|
|
635
690
|
|
|
636
691
|
let result;
|
|
@@ -785,6 +840,163 @@ export default function register(api: any) {
|
|
|
785
840
|
},
|
|
786
841
|
});
|
|
787
842
|
|
|
843
|
+
// ═══════════════════════════════════════════════════════
|
|
844
|
+
// TOOL: agether_health (comprehensive position health)
|
|
845
|
+
// ═══════════════════════════════════════════════════════
|
|
846
|
+
api.registerTool({
|
|
847
|
+
name: "agether_health",
|
|
848
|
+
description:
|
|
849
|
+
"Comprehensive health check: balances, all Morpho positions with LTV & liquidation risk, " +
|
|
850
|
+
"borrowing headroom, and current market rates — all in one call. " +
|
|
851
|
+
"Use this as the first call in any session to get full context before making decisions.",
|
|
852
|
+
parameters: { type: "object", properties: {}, required: [] },
|
|
853
|
+
async execute() {
|
|
854
|
+
try {
|
|
855
|
+
const cfg = getConfig(api);
|
|
856
|
+
const agetherCfg = api.config?.plugins?.entries?.agether?.config ?? {};
|
|
857
|
+
const healthThreshold = agetherCfg.healthAlertThreshold ?? 70;
|
|
858
|
+
const client = createClient(cfg);
|
|
859
|
+
|
|
860
|
+
// Gather everything in parallel where possible
|
|
861
|
+
const [balances, status, maxBorrow] = await Promise.all([
|
|
862
|
+
client.getBalances(),
|
|
863
|
+
client.getStatus(),
|
|
864
|
+
client.getMaxBorrowable(),
|
|
865
|
+
]);
|
|
866
|
+
|
|
867
|
+
// Build per-position health analysis
|
|
868
|
+
const positionHealth = status.positions.map((p: any) => {
|
|
869
|
+
const matchingMarket = maxBorrow.byMarket.find(
|
|
870
|
+
(m: any) => m.collateralToken === p.collateralToken,
|
|
871
|
+
);
|
|
872
|
+
const collateralValue = matchingMarket ? Number(matchingMarket.collateralValue) / 1e6 : 0;
|
|
873
|
+
const debt = parseFloat(p.debt);
|
|
874
|
+
const ltv = collateralValue > 0 ? (debt / collateralValue) * 100 : 0;
|
|
875
|
+
const maxLtv = 80; // Morpho LLTV
|
|
876
|
+
const headroom = matchingMarket ? Number(matchingMarket.maxAdditional) / 1e6 : 0;
|
|
877
|
+
|
|
878
|
+
let risk: string;
|
|
879
|
+
if (debt === 0) risk = "🟢 no debt";
|
|
880
|
+
else if (ltv >= maxLtv) risk = "🔴 LIQUIDATION ZONE";
|
|
881
|
+
else if (ltv >= healthThreshold) risk = "🟡 WARNING — approaching liquidation";
|
|
882
|
+
else risk = "🟢 healthy";
|
|
883
|
+
|
|
884
|
+
return {
|
|
885
|
+
collateralToken: p.collateralToken,
|
|
886
|
+
collateral: p.collateral,
|
|
887
|
+
collateralValueUsd: `$${collateralValue.toFixed(2)}`,
|
|
888
|
+
debt: `$${debt.toFixed(2)}`,
|
|
889
|
+
ltvPercent: `${ltv.toFixed(1)}%`,
|
|
890
|
+
maxLtv: `${maxLtv}%`,
|
|
891
|
+
headroomUsdc: `$${headroom.toFixed(2)}`,
|
|
892
|
+
risk,
|
|
893
|
+
};
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
const alerts: string[] = [];
|
|
897
|
+
for (const ph of positionHealth) {
|
|
898
|
+
if (ph.risk.includes("LIQUIDATION")) {
|
|
899
|
+
alerts.push(`🔴 ${ph.collateralToken}: LTV at ${ph.ltvPercent} — at risk of liquidation!`);
|
|
900
|
+
} else if (ph.risk.includes("WARNING")) {
|
|
901
|
+
alerts.push(`🟡 ${ph.collateralToken}: LTV at ${ph.ltvPercent} — consider adding collateral or repaying debt`);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// Check gas (ETH) is sufficient for transactions
|
|
906
|
+
const ethBalance = parseFloat(balances.eth);
|
|
907
|
+
if (ethBalance < 0.0005) {
|
|
908
|
+
alerts.push("⛽ Low ETH for gas — may fail on-chain transactions. Send ETH to EOA.");
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const result = {
|
|
912
|
+
agentId: balances.agentId,
|
|
913
|
+
walletAddress: balances.address,
|
|
914
|
+
balances: {
|
|
915
|
+
eoa: { eth: balances.eth, usdc: balances.usdc },
|
|
916
|
+
agentAccount: balances.agentAccount
|
|
917
|
+
? { address: balances.agentAccount.address, usdc: balances.agentAccount.usdc }
|
|
918
|
+
: null,
|
|
919
|
+
},
|
|
920
|
+
totalDebt: `$${status.totalDebt}`,
|
|
921
|
+
totalBorrowingHeadroom: `$${(Number(maxBorrow.total) / 1e6).toFixed(2)}`,
|
|
922
|
+
positions: positionHealth,
|
|
923
|
+
alerts: alerts.length > 0 ? alerts : ["✅ All positions healthy"],
|
|
924
|
+
rpcSource: process.env.ALCHEMY_API_KEY ? "Alchemy" :
|
|
925
|
+
process.env.ANKR_API_KEY ? "Ankr" :
|
|
926
|
+
process.env.QUICKNODE_URL ? "QuickNode" : "PublicNode (free)",
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
return ok(JSON.stringify(result, null, 2));
|
|
930
|
+
} catch (e) { return fail(e); }
|
|
931
|
+
},
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
// ═══════════════════════════════════════════════════════
|
|
935
|
+
// TOOL: agether_preflight (setup readiness check)
|
|
936
|
+
// ═══════════════════════════════════════════════════════
|
|
937
|
+
api.registerTool({
|
|
938
|
+
name: "agether_preflight",
|
|
939
|
+
description:
|
|
940
|
+
"Check that the agent is fully set up: private key present, RPC working, " +
|
|
941
|
+
"agent registered, balances non-zero. Returns a checklist of pass/fail items. " +
|
|
942
|
+
"Call this if anything seems wrong or on first-time setup.",
|
|
943
|
+
parameters: { type: "object", properties: {}, required: [] },
|
|
944
|
+
async execute() {
|
|
945
|
+
const checks: Array<{ check: string; status: string; detail?: string }> = [];
|
|
946
|
+
|
|
947
|
+
// 1. Private key
|
|
948
|
+
try {
|
|
949
|
+
resolvePrivateKey();
|
|
950
|
+
checks.push({ check: "Private key (AGETHER_PRIVATE_KEY)", status: "✅ set" });
|
|
951
|
+
} catch {
|
|
952
|
+
checks.push({ check: "Private key (AGETHER_PRIVATE_KEY)", status: "❌ missing", detail: "Set via: openclaw secrets configure → source: env → id: AGETHER_PRIVATE_KEY" });
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
// 2. RPC URL
|
|
956
|
+
const rpcUrl = resolveRpcUrl();
|
|
957
|
+
const rpcSource = process.env.ALCHEMY_API_KEY ? "Alchemy" :
|
|
958
|
+
process.env.ANKR_API_KEY ? "Ankr" :
|
|
959
|
+
process.env.QUICKNODE_URL ? "QuickNode" : "PublicNode (free fallback)";
|
|
960
|
+
checks.push({ check: "RPC endpoint", status: `✅ ${rpcSource}`, detail: rpcUrl.replace(/\/[a-zA-Z0-9_-]{20,}$/, '/***') });
|
|
961
|
+
|
|
962
|
+
// 3. Agent registration
|
|
963
|
+
try {
|
|
964
|
+
const cfg = getConfig(api);
|
|
965
|
+
const client = createClient(cfg);
|
|
966
|
+
const balances = await client.getBalances();
|
|
967
|
+
|
|
968
|
+
if (balances.agentId && balances.agentId !== '?') {
|
|
969
|
+
checks.push({ check: "Agent registration", status: `✅ ID: ${balances.agentId}` });
|
|
970
|
+
} else {
|
|
971
|
+
checks.push({ check: "Agent registration", status: "⚠️ not registered", detail: "Call agether_register to create an identity" });
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// 4. ETH for gas
|
|
975
|
+
const eth = parseFloat(balances.eth);
|
|
976
|
+
if (eth >= 0.001) {
|
|
977
|
+
checks.push({ check: "ETH for gas", status: `✅ ${eth.toFixed(6)} ETH` });
|
|
978
|
+
} else {
|
|
979
|
+
checks.push({ check: "ETH for gas", status: `⚠️ ${eth.toFixed(6)} ETH — low`, detail: `Send ETH to ${balances.address}` });
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// 5. USDC availability
|
|
983
|
+
const eoaUsdc = parseFloat(balances.usdc);
|
|
984
|
+
const safeUsdc = balances.agentAccount ? parseFloat(balances.agentAccount.usdc) : 0;
|
|
985
|
+
if (safeUsdc > 0) {
|
|
986
|
+
checks.push({ check: "USDC (AgentAccount)", status: `✅ $${safeUsdc.toFixed(2)}` });
|
|
987
|
+
} else if (eoaUsdc > 0) {
|
|
988
|
+
checks.push({ check: "USDC", status: `⚠️ $${eoaUsdc.toFixed(2)} in EOA only`, detail: "Use wallet_fund or morpho_deposit_and_borrow to get USDC into AgentAccount" });
|
|
989
|
+
} else {
|
|
990
|
+
checks.push({ check: "USDC", status: "❌ no USDC anywhere", detail: "Deposit collateral and borrow, or send USDC to EOA" });
|
|
991
|
+
}
|
|
992
|
+
} catch (e: any) {
|
|
993
|
+
checks.push({ check: "Blockchain connectivity", status: "❌ failed", detail: e.message });
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
return ok(JSON.stringify({ preflight: checks }, null, 2));
|
|
997
|
+
},
|
|
998
|
+
});
|
|
999
|
+
|
|
788
1000
|
// ═══════════════════════════════════════════════════════
|
|
789
1001
|
// SLASH COMMANDS (no AI needed)
|
|
790
1002
|
// ═══════════════════════════════════════════════════════
|
|
@@ -831,4 +1043,95 @@ export default function register(api: any) {
|
|
|
831
1043
|
}
|
|
832
1044
|
},
|
|
833
1045
|
});
|
|
1046
|
+
|
|
1047
|
+
api.registerCommand({
|
|
1048
|
+
name: "health",
|
|
1049
|
+
description: "Quick position health check — LTV, liquidation risk, balances (no AI)",
|
|
1050
|
+
handler: async () => {
|
|
1051
|
+
try {
|
|
1052
|
+
const cfg = getConfig(api);
|
|
1053
|
+
const agetherCfg = api.config?.plugins?.entries?.agether?.config ?? {};
|
|
1054
|
+
const healthThreshold = agetherCfg.healthAlertThreshold ?? 70;
|
|
1055
|
+
const client = createClient(cfg);
|
|
1056
|
+
|
|
1057
|
+
const [balances, status, maxBorrow] = await Promise.all([
|
|
1058
|
+
client.getBalances(),
|
|
1059
|
+
client.getStatus(),
|
|
1060
|
+
client.getMaxBorrowable(),
|
|
1061
|
+
]);
|
|
1062
|
+
|
|
1063
|
+
let text = `🏥 Health — Agent #${balances.agentId}\n`;
|
|
1064
|
+
text += `EOA: ${balances.eth} ETH, $${balances.usdc} USDC\n`;
|
|
1065
|
+
if (balances.agentAccount) {
|
|
1066
|
+
text += `Safe: $${balances.agentAccount.usdc} USDC\n`;
|
|
1067
|
+
}
|
|
1068
|
+
text += `Total debt: $${status.totalDebt}\n`;
|
|
1069
|
+
text += `Headroom: $${(Number(maxBorrow.total) / 1e6).toFixed(2)}\n`;
|
|
1070
|
+
|
|
1071
|
+
for (const p of status.positions) {
|
|
1072
|
+
const mm = maxBorrow.byMarket.find((m: any) => m.collateralToken === p.collateralToken);
|
|
1073
|
+
const cv = mm ? Number(mm.collateralValue) / 1e6 : 0;
|
|
1074
|
+
const debt = parseFloat(p.debt);
|
|
1075
|
+
const ltv = cv > 0 ? (debt / cv) * 100 : 0;
|
|
1076
|
+
const icon = debt === 0 ? "🟢" : ltv >= 80 ? "🔴" : ltv >= healthThreshold ? "🟡" : "🟢";
|
|
1077
|
+
text += `\n${icon} ${p.collateralToken}: ${p.collateral} col, $${p.debt} debt, LTV ${ltv.toFixed(1)}%`;
|
|
1078
|
+
}
|
|
1079
|
+
if (status.positions.length === 0) text += "\nNo active positions.";
|
|
1080
|
+
|
|
1081
|
+
return { text };
|
|
1082
|
+
} catch (e: any) {
|
|
1083
|
+
return { text: `❌ ${e.message}` };
|
|
1084
|
+
}
|
|
1085
|
+
},
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
api.registerCommand({
|
|
1089
|
+
name: "rates",
|
|
1090
|
+
description: "Show current Morpho market rates (no AI)",
|
|
1091
|
+
handler: async () => {
|
|
1092
|
+
try {
|
|
1093
|
+
const cfg = getConfig(api);
|
|
1094
|
+
const client = createClient(cfg);
|
|
1095
|
+
const rates = await client.getMarketRates();
|
|
1096
|
+
|
|
1097
|
+
if (rates.length === 0) return { text: "No markets found." };
|
|
1098
|
+
|
|
1099
|
+
let text = "📈 Morpho Rates\n";
|
|
1100
|
+
for (const r of rates as any[]) {
|
|
1101
|
+
text += `\n${r.collateralToken}/${r.loanToken}: borrow ${(r.borrowApy * 100).toFixed(2)}%, supply ${(r.supplyApy * 100).toFixed(2)}%, util ${(r.utilization * 100).toFixed(1)}%`;
|
|
1102
|
+
}
|
|
1103
|
+
return { text };
|
|
1104
|
+
} catch (e: any) {
|
|
1105
|
+
return { text: `❌ ${e.message}` };
|
|
1106
|
+
}
|
|
1107
|
+
},
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
// ═══════════════════════════════════════════════════════
|
|
1111
|
+
// HOOKS (automatic actions)
|
|
1112
|
+
// ═══════════════════════════════════════════════════════
|
|
1113
|
+
|
|
1114
|
+
api.registerHook(
|
|
1115
|
+
"command:new",
|
|
1116
|
+
async () => {
|
|
1117
|
+
// On new conversation, log a quick context summary so the agent
|
|
1118
|
+
// starts with awareness of the current state.
|
|
1119
|
+
try {
|
|
1120
|
+
const cfg = getConfig(api);
|
|
1121
|
+
const client = createClient(cfg);
|
|
1122
|
+
const balances = await client.getBalances();
|
|
1123
|
+
const agentId = balances.agentId ?? "?";
|
|
1124
|
+
const safeUsdc = balances.agentAccount?.usdc ?? "0";
|
|
1125
|
+
api.logger?.info?.(
|
|
1126
|
+
`[agether] Session start — Agent #${agentId}, EOA: ${balances.eth} ETH / $${balances.usdc} USDC, Safe: $${safeUsdc} USDC`,
|
|
1127
|
+
);
|
|
1128
|
+
} catch {
|
|
1129
|
+
// Silently fail — hook should not block conversations
|
|
1130
|
+
}
|
|
1131
|
+
},
|
|
1132
|
+
{
|
|
1133
|
+
name: "agether.session-context",
|
|
1134
|
+
description: "Logs agent balance context at the start of each new conversation",
|
|
1135
|
+
},
|
|
1136
|
+
);
|
|
834
1137
|
}
|
package/test/kya.test.ts
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* KYA gate feature — unit tests for plugin tools.
|
|
3
|
-
*
|
|
4
|
-
* Mocks MorphoClient so no blockchain connection is needed.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
8
|
-
|
|
9
|
-
// ── Mock state ──
|
|
10
|
-
|
|
11
|
-
const mockIsKyaRequired = vi.fn<() => Promise<boolean>>();
|
|
12
|
-
const mockRegister = vi.fn();
|
|
13
|
-
const mockGetBalances = vi.fn();
|
|
14
|
-
const mockGetAgentId = vi.fn().mockReturnValue('12345');
|
|
15
|
-
const mockGetAccountAddress = vi.fn().mockResolvedValue('0xAGENTACCOUNT');
|
|
16
|
-
|
|
17
|
-
vi.mock('@agether/sdk', () => {
|
|
18
|
-
// MorphoClient must work with `new` — use a class
|
|
19
|
-
class MockMorphoClient {
|
|
20
|
-
isKyaRequired = mockIsKyaRequired;
|
|
21
|
-
register = mockRegister;
|
|
22
|
-
getBalances = mockGetBalances;
|
|
23
|
-
getAgentId = mockGetAgentId;
|
|
24
|
-
getAccountAddress = mockGetAccountAddress;
|
|
25
|
-
}
|
|
26
|
-
class MockX402Client {}
|
|
27
|
-
return {
|
|
28
|
-
MorphoClient: MockMorphoClient,
|
|
29
|
-
X402Client: MockX402Client,
|
|
30
|
-
};
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
// Suppress fs.readFileSync for persistAgentId
|
|
34
|
-
vi.mock('fs', async (importOriginal) => {
|
|
35
|
-
const orig = await importOriginal<typeof import('fs')>();
|
|
36
|
-
return {
|
|
37
|
-
...orig,
|
|
38
|
-
default: {
|
|
39
|
-
...orig,
|
|
40
|
-
readFileSync: vi.fn().mockReturnValue('{}'),
|
|
41
|
-
writeFileSync: vi.fn(),
|
|
42
|
-
},
|
|
43
|
-
readFileSync: vi.fn().mockReturnValue('{}'),
|
|
44
|
-
writeFileSync: vi.fn(),
|
|
45
|
-
};
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
import registerPlugin from '../src/index';
|
|
49
|
-
|
|
50
|
-
// ── Helpers ──
|
|
51
|
-
|
|
52
|
-
/** Minimal mock for the OpenClaw plugin API */
|
|
53
|
-
function createMockApi(config?: Record<string, any>) {
|
|
54
|
-
const tools = new Map<string, { execute: Function }>();
|
|
55
|
-
return {
|
|
56
|
-
config: {
|
|
57
|
-
plugins: {
|
|
58
|
-
entries: {
|
|
59
|
-
agether: {
|
|
60
|
-
config: {
|
|
61
|
-
privateKey: '0x' + 'ab'.repeat(32),
|
|
62
|
-
agentId: '12345',
|
|
63
|
-
rpcUrl: 'http://127.0.0.1:8545',
|
|
64
|
-
backendUrl: 'http://localhost:3001',
|
|
65
|
-
...config,
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
|
-
registerTool(def: { name: string; execute: Function }) {
|
|
72
|
-
tools.set(def.name, def);
|
|
73
|
-
},
|
|
74
|
-
registerCommand() { /* noop */ },
|
|
75
|
-
getTool(name: string) {
|
|
76
|
-
return tools.get(name);
|
|
77
|
-
},
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// ── Tests ──
|
|
82
|
-
|
|
83
|
-
describe('agether_kya_status tool', () => {
|
|
84
|
-
let api: ReturnType<typeof createMockApi>;
|
|
85
|
-
|
|
86
|
-
beforeEach(() => {
|
|
87
|
-
vi.clearAllMocks();
|
|
88
|
-
api = createMockApi();
|
|
89
|
-
registerPlugin(api);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('returns DISABLED message when isKyaRequired = false', async () => {
|
|
93
|
-
mockIsKyaRequired.mockResolvedValue(false);
|
|
94
|
-
|
|
95
|
-
const tool = api.getTool('agether_kya_status')!;
|
|
96
|
-
const result = await tool.execute();
|
|
97
|
-
|
|
98
|
-
expect(result.content[0].text).toContain('DISABLED');
|
|
99
|
-
expect(result.isError).toBeUndefined();
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('returns ENABLED message when isKyaRequired = true', async () => {
|
|
103
|
-
mockIsKyaRequired.mockResolvedValue(true);
|
|
104
|
-
|
|
105
|
-
const tool = api.getTool('agether_kya_status')!;
|
|
106
|
-
const result = await tool.execute();
|
|
107
|
-
|
|
108
|
-
expect(result.content[0].text).toContain('ENABLED');
|
|
109
|
-
expect(result.isError).toBeUndefined();
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
describe('agether_register kyaMessage', () => {
|
|
114
|
-
let api: ReturnType<typeof createMockApi>;
|
|
115
|
-
|
|
116
|
-
beforeEach(() => {
|
|
117
|
-
vi.clearAllMocks();
|
|
118
|
-
api = createMockApi();
|
|
119
|
-
registerPlugin(api);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it('includes "gate disabled" kyaMessage when kyaRequired = false', async () => {
|
|
123
|
-
mockRegister.mockResolvedValue({
|
|
124
|
-
agentId: '99999',
|
|
125
|
-
address: '0xWALLET',
|
|
126
|
-
agentAccount: '0xACCOUNT',
|
|
127
|
-
alreadyRegistered: false,
|
|
128
|
-
kyaRequired: false,
|
|
129
|
-
tx: '0xTXHASH',
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
const tool = api.getTool('agether_register')!;
|
|
133
|
-
const result = await tool.execute('test-id', { name: 'TestAgent' });
|
|
134
|
-
const data = JSON.parse(result.content[0].text);
|
|
135
|
-
|
|
136
|
-
expect(data.kyaRequired).toBe(false);
|
|
137
|
-
expect(data.kyaMessage).toContain('gate disabled');
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('includes "KYA required" kyaMessage when kyaRequired = true', async () => {
|
|
141
|
-
mockRegister.mockResolvedValue({
|
|
142
|
-
agentId: '99999',
|
|
143
|
-
address: '0xWALLET',
|
|
144
|
-
agentAccount: '0xACCOUNT',
|
|
145
|
-
alreadyRegistered: false,
|
|
146
|
-
kyaRequired: true,
|
|
147
|
-
tx: '0xTXHASH',
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
const tool = api.getTool('agether_register')!;
|
|
151
|
-
const result = await tool.execute('test-id', { name: 'TestAgent' });
|
|
152
|
-
const data = JSON.parse(result.content[0].text);
|
|
153
|
-
|
|
154
|
-
expect(data.kyaRequired).toBe(true);
|
|
155
|
-
expect(data.kyaMessage).toContain('KYA required');
|
|
156
|
-
});
|
|
157
|
-
});
|