dolomite-cli 1.0.0__tar.gz

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.
@@ -0,0 +1,57 @@
1
+ # === Secrets & credentials ===
2
+ .env
3
+ *.env
4
+ **/.env
5
+ **/config.json
6
+ !projects/pokemon-alerts/sku-database.json
7
+ secrets/
8
+ tokens/
9
+
10
+ # === OpenClaw config (contains API keys) ===
11
+ openclaw.json
12
+
13
+ # === Temp & cache ===
14
+ __pycache__/
15
+ *.pyc
16
+ *.pyo
17
+ .DS_Store
18
+ *.swp
19
+ *.swo
20
+ *~
21
+ .cache/
22
+ tmp/
23
+ *.tmp
24
+
25
+ # === Node modules ===
26
+ node_modules/
27
+
28
+ # === Large binary/media files ===
29
+ *.ogg
30
+ *.wav
31
+ *.mp3
32
+ *.mp4
33
+ *.jpg
34
+ *.jpeg
35
+ *.png
36
+ *.gif
37
+ *.zip
38
+ *.tar.gz
39
+
40
+ # === Runtime state (changes every poll cycle) ===
41
+ projects/pokemon-alerts/data/poller_state.json
42
+ projects/pokemon-alerts/data/server_comparison.json
43
+ dashboard/pokemon-scan-results.json
44
+ dashboard/pokemon-geocache.json
45
+ memory/heartbeat-state.json
46
+ memory/usage-tracking.json
47
+
48
+ # === Sensitive memory files ===
49
+ # MEMORY.md contains personal info - keep out of public repos
50
+ # But we want it in private repo for backup
51
+ # (repo is private, so this is fine)
52
+
53
+ # === Codecks data (contains tokens in practice) ===
54
+ codecks/*.json
55
+ llmrouter-app/
56
+ dg-scan/
57
+ bn_pokemon.json
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 OpenClaw
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,127 @@
1
+ Metadata-Version: 2.4
2
+ Name: dolomite-cli
3
+ Version: 1.0.0
4
+ Summary: CLI tool for querying Dolomite protocol data across all chains
5
+ Project-URL: Homepage, https://github.com/openclaw/dolomite-cli
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Keywords: blockchain,cli,defi,dolomite,interest-rates
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Office/Business :: Financial
19
+ Requires-Python: >=3.10
20
+ Description-Content-Type: text/markdown
21
+
22
+ # dolomite CLI
23
+
24
+ Query Dolomite protocol data across all chains. Returns structured JSON. No API keys required.
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ pipx install dolomite-cli
30
+ ```
31
+
32
+ Or with pip:
33
+
34
+ ```bash
35
+ pip install dolomite-cli
36
+ ```
37
+
38
+ Requires Python 3.10+. Zero dependencies (stdlib only).
39
+
40
+ ### From source
41
+
42
+ ```bash
43
+ git clone https://github.com/openclaw/dolomite-cli
44
+ cd dolomite-cli
45
+ pip install -e .
46
+ ```
47
+
48
+ ## Commands
49
+
50
+ | Command | Description |
51
+ |---------|-------------|
52
+ | `dolomite rates` | All markets with supply/borrow rates |
53
+ | `dolomite positions` | Top borrowing positions by size |
54
+ | `dolomite flows` | Recent large deposits/withdrawals |
55
+ | `dolomite liquidations` | Recent liquidation events |
56
+ | `dolomite tvl` | Protocol TVL summary per chain |
57
+ | `dolomite markets --token USDC` | Detailed info for a specific token |
58
+ | `dolomite account <address>` | Full position detail for an address |
59
+ | `dolomite risks` | High-risk positions and utilization alerts |
60
+ | `dolomite schema` | Show all commands, chains, entities, examples |
61
+
62
+ ## Examples
63
+
64
+ ```bash
65
+ # Stablecoin rates across all chains
66
+ dolomite rates --stables-only
67
+
68
+ # Top 20 positions on Ethereum
69
+ dolomite positions --chain ethereum --top 20
70
+
71
+ # Whale flows in the last 48 hours
72
+ dolomite flows --hours 48 --min-usd 100000
73
+
74
+ # Deposits only
75
+ dolomite flows --type deposit --hours 72
76
+
77
+ # Look up a specific whale
78
+ dolomite account 0x8be46b25d59616e594f0a9e20147fb14c1b989d9
79
+
80
+ # High-risk positions (>80% LTV, >$100K)
81
+ dolomite risks --min-ltv 80 --min-usd 100000
82
+
83
+ # Berachain rates only
84
+ dolomite rates --chain berachain
85
+
86
+ # USDC across all chains
87
+ dolomite markets --token USDC
88
+ ```
89
+
90
+ ## Output
91
+
92
+ All commands output JSON to stdout. Errors go to stderr. Pipe to `jq` for filtering:
93
+
94
+ ```bash
95
+ # Top 5 stablecoin rates
96
+ dolomite rates --stables-only | jq '.markets[:5][] | {chain, symbol, supply_rate_pct}'
97
+
98
+ # Total protocol TVL
99
+ dolomite tvl | jq '.total_supply_usd'
100
+
101
+ # Addresses with >85% LTV
102
+ dolomite risks --min-ltv 85 | jq '.high_ltv_positions[] | {address, ltv_pct, supply_usd}'
103
+ ```
104
+
105
+ ## Chains
106
+
107
+ | Chain | Chain ID | Status |
108
+ |-------|----------|--------|
109
+ | Ethereum | 1 | Active (~$370M TVL) |
110
+ | Arbitrum | 42161 | Active (~$52M TVL) |
111
+ | Berachain | 80094 | Active (~$50M TVL) |
112
+ | Mantle | 5000 | Minimal |
113
+ | Base | 8453 | Minimal |
114
+ | X Layer | 196 | Minimal |
115
+
116
+ Default: queries Ethereum, Arbitrum, Berachain. Use `--chain all` for everything.
117
+
118
+ ## Data Sources
119
+
120
+ - **Subgraph API**: `subgraph.api.dolomite.io` — on-chain positions, flows, liquidations
121
+ - **REST API**: `api.dolomite.io` — token prices, interest rates, market data
122
+
123
+ No authentication required. All data is public.
124
+
125
+ ## For AI Agents
126
+
127
+ Run `dolomite schema` to get a complete description of all commands, available entities, and example queries. The output is JSON — parseable by any model.
@@ -0,0 +1,106 @@
1
+ # dolomite CLI
2
+
3
+ Query Dolomite protocol data across all chains. Returns structured JSON. No API keys required.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pipx install dolomite-cli
9
+ ```
10
+
11
+ Or with pip:
12
+
13
+ ```bash
14
+ pip install dolomite-cli
15
+ ```
16
+
17
+ Requires Python 3.10+. Zero dependencies (stdlib only).
18
+
19
+ ### From source
20
+
21
+ ```bash
22
+ git clone https://github.com/openclaw/dolomite-cli
23
+ cd dolomite-cli
24
+ pip install -e .
25
+ ```
26
+
27
+ ## Commands
28
+
29
+ | Command | Description |
30
+ |---------|-------------|
31
+ | `dolomite rates` | All markets with supply/borrow rates |
32
+ | `dolomite positions` | Top borrowing positions by size |
33
+ | `dolomite flows` | Recent large deposits/withdrawals |
34
+ | `dolomite liquidations` | Recent liquidation events |
35
+ | `dolomite tvl` | Protocol TVL summary per chain |
36
+ | `dolomite markets --token USDC` | Detailed info for a specific token |
37
+ | `dolomite account <address>` | Full position detail for an address |
38
+ | `dolomite risks` | High-risk positions and utilization alerts |
39
+ | `dolomite schema` | Show all commands, chains, entities, examples |
40
+
41
+ ## Examples
42
+
43
+ ```bash
44
+ # Stablecoin rates across all chains
45
+ dolomite rates --stables-only
46
+
47
+ # Top 20 positions on Ethereum
48
+ dolomite positions --chain ethereum --top 20
49
+
50
+ # Whale flows in the last 48 hours
51
+ dolomite flows --hours 48 --min-usd 100000
52
+
53
+ # Deposits only
54
+ dolomite flows --type deposit --hours 72
55
+
56
+ # Look up a specific whale
57
+ dolomite account 0x8be46b25d59616e594f0a9e20147fb14c1b989d9
58
+
59
+ # High-risk positions (>80% LTV, >$100K)
60
+ dolomite risks --min-ltv 80 --min-usd 100000
61
+
62
+ # Berachain rates only
63
+ dolomite rates --chain berachain
64
+
65
+ # USDC across all chains
66
+ dolomite markets --token USDC
67
+ ```
68
+
69
+ ## Output
70
+
71
+ All commands output JSON to stdout. Errors go to stderr. Pipe to `jq` for filtering:
72
+
73
+ ```bash
74
+ # Top 5 stablecoin rates
75
+ dolomite rates --stables-only | jq '.markets[:5][] | {chain, symbol, supply_rate_pct}'
76
+
77
+ # Total protocol TVL
78
+ dolomite tvl | jq '.total_supply_usd'
79
+
80
+ # Addresses with >85% LTV
81
+ dolomite risks --min-ltv 85 | jq '.high_ltv_positions[] | {address, ltv_pct, supply_usd}'
82
+ ```
83
+
84
+ ## Chains
85
+
86
+ | Chain | Chain ID | Status |
87
+ |-------|----------|--------|
88
+ | Ethereum | 1 | Active (~$370M TVL) |
89
+ | Arbitrum | 42161 | Active (~$52M TVL) |
90
+ | Berachain | 80094 | Active (~$50M TVL) |
91
+ | Mantle | 5000 | Minimal |
92
+ | Base | 8453 | Minimal |
93
+ | X Layer | 196 | Minimal |
94
+
95
+ Default: queries Ethereum, Arbitrum, Berachain. Use `--chain all` for everything.
96
+
97
+ ## Data Sources
98
+
99
+ - **Subgraph API**: `subgraph.api.dolomite.io` — on-chain positions, flows, liquidations
100
+ - **REST API**: `api.dolomite.io` — token prices, interest rates, market data
101
+
102
+ No authentication required. All data is public.
103
+
104
+ ## For AI Agents
105
+
106
+ Run `dolomite schema` to get a complete description of all commands, available entities, and example queries. The output is JSON — parseable by any model.
@@ -0,0 +1,891 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ dolomite — CLI tool for querying Dolomite protocol data across all chains.
4
+
5
+ Provides structured JSON output of rates, positions, flows, liquidations,
6
+ TVL, market details, and account portfolios from Dolomite's subgraphs and APIs.
7
+
8
+ No API keys required. All data is public on-chain.
9
+
10
+ Usage:
11
+ dolomite rates [--chain CHAIN] [--stables-only] [--min-tvl N]
12
+ dolomite positions [--chain CHAIN] [--top N] [--min-usd N]
13
+ dolomite flows [--chain CHAIN] [--hours N] [--min-usd N] [--type deposit|withdrawal]
14
+ dolomite liquidations [--chain CHAIN] [--hours N] [--limit N]
15
+ dolomite tvl
16
+ dolomite markets [--token SYMBOL] [--chain CHAIN]
17
+ dolomite account <address> [--chain CHAIN]
18
+ dolomite risks [--min-ltv N] [--min-usd N]
19
+ dolomite schema
20
+ """
21
+
22
+ import argparse
23
+ import json
24
+ import sys
25
+ import time
26
+ from datetime import datetime, timezone
27
+ from urllib.request import Request, urlopen
28
+ from urllib.error import HTTPError, URLError
29
+
30
+ __version__ = "1.0.0"
31
+
32
+ # ─── Constants ────────────────────────────────────────────────────────────────
33
+
34
+ SUBGRAPH_BASE = "https://subgraph.api.dolomite.io/api/public/1301d2d1-7a9d-4be4-9e9a-061cb8611549/subgraphs"
35
+ API_BASE = "https://api.dolomite.io"
36
+
37
+ CHAINS = {
38
+ "ethereum": {"name": "Ethereum", "subgraph": "dolomite-ethereum", "chain_id": 1},
39
+ "arbitrum": {"name": "Arbitrum", "subgraph": "dolomite-arbitrum", "chain_id": 42161},
40
+ "berachain": {"name": "Berachain", "subgraph": "dolomite-berachain-mainnet", "chain_id": 80094},
41
+ "mantle": {"name": "Mantle", "subgraph": "dolomite-mantle", "chain_id": 5000},
42
+ "base": {"name": "Base", "subgraph": "dolomite-base", "chain_id": 8453},
43
+ "xlayer": {"name": "X Layer", "subgraph": "dolomite-x-layer", "chain_id": 196},
44
+ "botanix": {"name": "Botanix", "subgraph": "dolomite-botanix", "chain_id": 3637},
45
+ "polygonzkevm": {"name": "Polygon zkEVM", "subgraph": "dolomite-polygon-zk-evm", "chain_id": 1101},
46
+ }
47
+
48
+ ACTIVE_CHAINS = ["ethereum", "arbitrum", "berachain", "mantle", "botanix"] # all chains except X Layer and Polygon zkEVM (older subgraph schema)
49
+
50
+ STABLECOINS = {
51
+ "USDC", "USDC.e", "USDT", "DAI", "USD1", "HONEY", "BYUSD",
52
+ "savUSD", "sDAI", "LUSD", "USD₮0", "rUSD", "MIM", "FRAX"
53
+ }
54
+
55
+ # ─── Helpers ──────────────────────────────────────────────────────────────────
56
+
57
+ def _http_json(url, payload=None, timeout=20):
58
+ """Make an HTTP request and return parsed JSON."""
59
+ if payload:
60
+ data = json.dumps(payload).encode("utf-8")
61
+ req = Request(url, data=data, headers={"Content-Type": "application/json"})
62
+ else:
63
+ req = Request(url)
64
+ try:
65
+ with urlopen(req, timeout=timeout) as resp:
66
+ return json.loads(resp.read())
67
+ except (HTTPError, URLError, TimeoutError) as e:
68
+ _err(f"HTTP error: {e}")
69
+ return {}
70
+
71
+
72
+ def _gql(subgraph_name, query, variables=None, timeout=20):
73
+ """Execute a GraphQL query against a Dolomite subgraph."""
74
+ url = f"{SUBGRAPH_BASE}/{subgraph_name}/latest/gn"
75
+ payload = {"query": query}
76
+ if variables:
77
+ payload["variables"] = variables
78
+ result = _http_json(url, payload, timeout)
79
+ if "errors" in result:
80
+ _err(f"GraphQL errors: {result['errors']}")
81
+ return result.get("data", {})
82
+
83
+
84
+ def _api(path, timeout=15):
85
+ """Fetch from Dolomite REST API."""
86
+ return _http_json(f"{API_BASE}{path}", timeout=timeout)
87
+
88
+
89
+ def _out(data):
90
+ """Print JSON output to stdout."""
91
+ json.dump(data, sys.stdout, indent=2)
92
+ sys.stdout.write("\n")
93
+
94
+
95
+ def _err(msg):
96
+ """Print error to stderr."""
97
+ print(f"error: {msg}", file=sys.stderr)
98
+
99
+
100
+ def _resolve_chains(chain_arg):
101
+ """Resolve --chain argument to list of chain keys."""
102
+ if chain_arg:
103
+ key = chain_arg.lower()
104
+ if key == "all":
105
+ return list(CHAINS.keys())
106
+ if key not in CHAINS:
107
+ _err(f"Unknown chain '{key}'. Available: {', '.join(CHAINS.keys())}")
108
+ sys.exit(1)
109
+ return [key]
110
+ return ACTIVE_CHAINS
111
+
112
+
113
+ def _ts_hours_ago(hours):
114
+ """Return unix timestamp N hours ago."""
115
+ return int(time.time()) - (hours * 3600)
116
+
117
+
118
+ # ─── Commands ─────────────────────────────────────────────────────────────────
119
+
120
+ def cmd_rates(args):
121
+ """Fetch all markets with supply/borrow rates across chains."""
122
+ chains = _resolve_chains(args.chain)
123
+ min_tvl = args.min_tvl or 1000
124
+ markets = []
125
+
126
+ for chain_key in chains:
127
+ ci = CHAINS[chain_key]
128
+ try:
129
+ tokens_resp = _api(f"/tokens/{ci['chain_id']}")
130
+ rates_resp = _api(f"/tokens/{ci['chain_id']}/interest-rates")
131
+ prices_resp = _api(f"/tokens/{ci['chain_id']}/prices")
132
+
133
+ tokens = {t["id"]: t for t in tokens_resp.get("tokens", [])}
134
+ rates = {r["token"]["tokenAddress"]: r for r in rates_resp.get("interestRates", [])}
135
+ prices = prices_resp.get("prices", {})
136
+
137
+ for addr, token in tokens.items():
138
+ rate = rates.get(addr, {})
139
+ price = float(prices.get(addr, 0))
140
+ supply = float(token.get("supplyLiquidity", 0))
141
+ borrow = float(token.get("borrowLiquidity", 0))
142
+ supply_rate = float(rate.get("totalSupplyInterestRate", 0)) * 100
143
+ borrow_rate = float(rate.get("borrowInterestRate", 0)) * 100
144
+ util = (borrow / supply * 100) if supply > 0 else 0
145
+ supply_usd = supply * price
146
+ borrow_usd = borrow * price
147
+ is_stable = token["symbol"] in STABLECOINS
148
+
149
+ if supply_usd < min_tvl:
150
+ continue
151
+ if args.stables_only and not is_stable:
152
+ continue
153
+
154
+ markets.append({
155
+ "chain": ci["name"],
156
+ "symbol": token["symbol"],
157
+ "supply_rate_pct": round(supply_rate, 3),
158
+ "borrow_rate_pct": round(borrow_rate, 3),
159
+ "utilization_pct": round(util, 1),
160
+ "supply_usd": round(supply_usd),
161
+ "borrow_usd": round(borrow_usd),
162
+ "price_usd": round(price, 4),
163
+ "is_stablecoin": is_stable,
164
+ })
165
+ except Exception as e:
166
+ _err(f"Failed to fetch {ci['name']} rates: {e}")
167
+
168
+ markets.sort(key=lambda x: -x["supply_usd"])
169
+ _out({"timestamp": datetime.now(timezone.utc).isoformat(), "count": len(markets), "markets": markets})
170
+
171
+
172
+ def cmd_positions(args):
173
+ """Fetch top borrowing positions by size."""
174
+ chains = _resolve_chains(args.chain)
175
+ top_n = args.top or 50
176
+ min_usd = args.min_usd or 10000
177
+ positions = []
178
+
179
+ for chain_key in chains:
180
+ ci = CHAINS[chain_key]
181
+ try:
182
+ # Fetch in batches — subgraph max is 1000
183
+ limit = min(top_n * 2, 1000) # fetch extra, filter later
184
+ d = _gql(ci["subgraph"], f"""{{
185
+ marginAccounts(
186
+ where: {{hasBorrowValue: true}}
187
+ orderBy: lastUpdatedTimestamp
188
+ orderDirection: desc
189
+ first: {limit}
190
+ ) {{
191
+ id
192
+ effectiveUser {{ id }}
193
+ accountNumber
194
+ lastUpdatedTimestamp
195
+ tokenValues(first: 30) {{
196
+ token {{ symbol id }}
197
+ valuePar
198
+ }}
199
+ borrowTokens {{ symbol }}
200
+ supplyTokens {{ symbol }}
201
+ }}
202
+ }}""", timeout=30)
203
+
204
+ prices = _api(f"/tokens/{ci['chain_id']}/prices").get("prices", {})
205
+
206
+ for acct in d.get("marginAccounts", []):
207
+ supply_usd = 0
208
+ borrow_usd = 0
209
+ position_detail = []
210
+
211
+ for tv in acct["tokenValues"]:
212
+ val = float(tv["valuePar"])
213
+ price = float(prices.get(tv["token"]["id"], 0))
214
+ usd = val * price
215
+ if val > 0:
216
+ supply_usd += usd
217
+ else:
218
+ borrow_usd += abs(usd)
219
+ if abs(usd) > 100:
220
+ position_detail.append({
221
+ "token": tv["token"]["symbol"],
222
+ "amount": round(val, 4),
223
+ "usd": round(usd, 2),
224
+ })
225
+
226
+ if supply_usd < min_usd and borrow_usd < min_usd:
227
+ continue
228
+
229
+ ltv = (borrow_usd / supply_usd * 100) if supply_usd > 0 else 0
230
+ positions.append({
231
+ "chain": ci["name"],
232
+ "address": acct["effectiveUser"]["id"],
233
+ "account_number": acct["accountNumber"],
234
+ "supply_usd": round(supply_usd),
235
+ "borrow_usd": round(borrow_usd),
236
+ "net_usd": round(supply_usd - borrow_usd),
237
+ "ltv_pct": round(ltv, 1),
238
+ "supply_tokens": [t["symbol"] for t in acct["supplyTokens"]],
239
+ "borrow_tokens": [t["symbol"] for t in acct["borrowTokens"]],
240
+ "positions": position_detail,
241
+ "last_active": int(acct["lastUpdatedTimestamp"]),
242
+ })
243
+ except Exception as e:
244
+ _err(f"Failed to fetch {ci['name']} positions: {e}")
245
+
246
+ positions.sort(key=lambda x: -x["supply_usd"])
247
+ positions = positions[:top_n]
248
+ _out({"timestamp": datetime.now(timezone.utc).isoformat(), "count": len(positions), "positions": positions})
249
+
250
+
251
+ def cmd_flows(args):
252
+ """Fetch recent large deposits and withdrawals."""
253
+ chains = _resolve_chains(args.chain)
254
+ hours = args.hours or 24
255
+ min_usd = args.min_usd or 50000
256
+ flow_type = args.type # deposit, withdrawal, or None (both)
257
+ ts_floor = _ts_hours_ago(hours)
258
+ flows = []
259
+
260
+ for chain_key in chains:
261
+ ci = CHAINS[chain_key]
262
+ try:
263
+ queries = []
264
+ if flow_type != "withdrawal":
265
+ queries.append(("deposit", f"""{{
266
+ deposits(
267
+ orderBy: serialId, orderDirection: desc, first: 100,
268
+ where: {{amountUSDDeltaWei_gte: "{min_usd}"}}
269
+ ) {{
270
+ transaction {{ timestamp blockNumber }}
271
+ marginAccount {{ effectiveUser {{ id }} }}
272
+ token {{ symbol }}
273
+ amountDeltaWei
274
+ amountUSDDeltaWei
275
+ }}
276
+ }}"""))
277
+ if flow_type != "deposit":
278
+ queries.append(("withdrawal", f"""{{
279
+ withdrawals(
280
+ orderBy: serialId, orderDirection: desc, first: 100,
281
+ where: {{amountUSDDeltaWei_gte: "{min_usd}"}}
282
+ ) {{
283
+ transaction {{ timestamp blockNumber }}
284
+ marginAccount {{ effectiveUser {{ id }} }}
285
+ token {{ symbol }}
286
+ amountDeltaWei
287
+ amountUSDDeltaWei
288
+ }}
289
+ }}"""))
290
+
291
+ for ftype, query in queries:
292
+ d = _gql(ci["subgraph"], query, timeout=20)
293
+ entity_key = "deposits" if ftype == "deposit" else "withdrawals"
294
+ for item in d.get(entity_key, []):
295
+ ts = int(item["transaction"]["timestamp"])
296
+ if ts < ts_floor:
297
+ continue
298
+ flows.append({
299
+ "type": ftype,
300
+ "chain": ci["name"],
301
+ "address": item["marginAccount"]["effectiveUser"]["id"],
302
+ "token": item["token"]["symbol"],
303
+ "amount": round(float(item["amountDeltaWei"]), 4),
304
+ "usd": round(float(item["amountUSDDeltaWei"])),
305
+ "timestamp": ts,
306
+ "block": int(item["transaction"]["blockNumber"]),
307
+ "time": datetime.fromtimestamp(ts, tz=timezone.utc).isoformat(),
308
+ })
309
+ except Exception as e:
310
+ _err(f"Failed to fetch {ci['name']} flows: {e}")
311
+
312
+ flows.sort(key=lambda x: -x["timestamp"])
313
+ _out({
314
+ "timestamp": datetime.now(timezone.utc).isoformat(),
315
+ "period_hours": hours,
316
+ "min_usd": min_usd,
317
+ "count": len(flows),
318
+ "flows": flows,
319
+ })
320
+
321
+
322
+ def cmd_liquidations(args):
323
+ """Fetch recent liquidation events."""
324
+ chains = _resolve_chains(args.chain)
325
+ hours = args.hours or 168 # default 7 days
326
+ limit = args.limit or 100
327
+ ts_floor = _ts_hours_ago(hours)
328
+ liquidations = []
329
+
330
+ for chain_key in chains:
331
+ ci = CHAINS[chain_key]
332
+ try:
333
+ d = _gql(ci["subgraph"], f"""{{
334
+ liquidations(
335
+ orderBy: serialId, orderDirection: desc, first: {min(limit, 200)}
336
+ ) {{
337
+ transaction {{ timestamp blockNumber }}
338
+ solidMarginAccount {{ effectiveUser {{ id }} }}
339
+ liquidMarginAccount {{ effectiveUser {{ id }} }}
340
+ heldToken {{ symbol }}
341
+ borrowedToken {{ symbol }}
342
+ heldTokenAmountDeltaWei
343
+ borrowedTokenAmountDeltaWei
344
+ heldTokenAmountUSD
345
+ borrowedTokenAmountUSD
346
+ heldTokenLiquidationRewardWei
347
+ heldTokenLiquidationRewardUSD
348
+ }}
349
+ }}""", timeout=20)
350
+
351
+ prices = _api(f"/tokens/{ci['chain_id']}/prices").get("prices", {})
352
+
353
+ for liq in d.get("liquidations", []):
354
+ ts = int(liq["transaction"]["timestamp"])
355
+ if ts < ts_floor:
356
+ continue
357
+ liquidations.append({
358
+ "chain": ci["name"],
359
+ "liquidated_address": liq["liquidMarginAccount"]["effectiveUser"]["id"],
360
+ "liquidator_address": liq["solidMarginAccount"]["effectiveUser"]["id"],
361
+ "held_token": liq["heldToken"]["symbol"],
362
+ "borrowed_token": liq["borrowedToken"]["symbol"],
363
+ "held_usd": round(float(liq.get("heldTokenAmountUSD", 0)), 2),
364
+ "borrowed_usd": round(float(liq.get("borrowedTokenAmountUSD", 0)), 2),
365
+ "reward_usd": round(float(liq.get("heldTokenLiquidationRewardUSD", 0)), 2),
366
+ "timestamp": ts,
367
+ "block": int(liq["transaction"]["blockNumber"]),
368
+ "time": datetime.fromtimestamp(ts, tz=timezone.utc).isoformat(),
369
+ })
370
+ except Exception as e:
371
+ _err(f"Failed to fetch {ci['name']} liquidations: {e}")
372
+
373
+ liquidations.sort(key=lambda x: -x["timestamp"])
374
+ _out({
375
+ "timestamp": datetime.now(timezone.utc).isoformat(),
376
+ "period_hours": hours,
377
+ "count": len(liquidations),
378
+ "liquidations": liquidations,
379
+ })
380
+
381
+
382
+ def cmd_tvl(args):
383
+ """Fetch protocol-level TVL summary per chain."""
384
+ chains = _resolve_chains(getattr(args, 'chain', None))
385
+ summary = {}
386
+
387
+ for chain_key in chains:
388
+ ci = CHAINS[chain_key]
389
+ try:
390
+ d = _gql(ci["subgraph"], """{
391
+ dolomiteMargins(first: 1) {
392
+ supplyLiquidityUSD borrowLiquidityUSD userCount
393
+ totalTradeVolumeUSD totalLiquidationVolumeUSD
394
+ liquidationCount borrowPositionCount numberOfMarkets
395
+ }
396
+ }""")
397
+ margin = d["dolomiteMargins"][0]
398
+ supply = float(margin["supplyLiquidityUSD"])
399
+ borrow = float(margin["borrowLiquidityUSD"])
400
+ summary[ci["name"]] = {
401
+ "supply_usd": round(supply),
402
+ "borrow_usd": round(borrow),
403
+ "utilization_pct": round(borrow / supply * 100, 1) if supply > 0 else 0,
404
+ "users": int(margin["userCount"]),
405
+ "markets": margin["numberOfMarkets"],
406
+ "borrow_positions": int(margin["borrowPositionCount"]),
407
+ "total_trade_volume_usd": round(float(margin["totalTradeVolumeUSD"])),
408
+ "total_liquidation_volume_usd": round(float(margin["totalLiquidationVolumeUSD"])),
409
+ "liquidation_count": int(margin["liquidationCount"]),
410
+ }
411
+ except Exception as e:
412
+ _err(f"Failed to fetch {ci['name']} TVL: {e}")
413
+
414
+ total_supply = sum(v["supply_usd"] for v in summary.values())
415
+ total_borrow = sum(v["borrow_usd"] for v in summary.values())
416
+ total_users = sum(v["users"] for v in summary.values())
417
+
418
+ _out({
419
+ "timestamp": datetime.now(timezone.utc).isoformat(),
420
+ "total_supply_usd": total_supply,
421
+ "total_borrow_usd": total_borrow,
422
+ "total_utilization_pct": round(total_borrow / total_supply * 100, 1) if total_supply > 0 else 0,
423
+ "total_users": total_users,
424
+ "chains": summary,
425
+ })
426
+
427
+
428
+ def cmd_markets(args):
429
+ """Fetch detailed info for a specific token across chains."""
430
+ chains = _resolve_chains(args.chain)
431
+ target = args.token.upper() if args.token else None
432
+ results = []
433
+
434
+ for chain_key in chains:
435
+ ci = CHAINS[chain_key]
436
+ try:
437
+ tokens_resp = _api(f"/tokens/{ci['chain_id']}")
438
+ rates_resp = _api(f"/tokens/{ci['chain_id']}/interest-rates")
439
+ prices_resp = _api(f"/tokens/{ci['chain_id']}/prices")
440
+
441
+ tokens = {t["id"]: t for t in tokens_resp.get("tokens", [])}
442
+ rates = {r["token"]["tokenAddress"]: r for r in rates_resp.get("interestRates", [])}
443
+ prices = prices_resp.get("prices", {})
444
+
445
+ for addr, token in tokens.items():
446
+ if target and token["symbol"].upper() != target:
447
+ continue
448
+ rate = rates.get(addr, {})
449
+ price = float(prices.get(addr, 0))
450
+ supply = float(token.get("supplyLiquidity", 0))
451
+ borrow = float(token.get("borrowLiquidity", 0))
452
+ supply_rate = float(rate.get("totalSupplyInterestRate", 0)) * 100
453
+ borrow_rate = float(rate.get("borrowInterestRate", 0)) * 100
454
+ util = (borrow / supply * 100) if supply > 0 else 0
455
+
456
+ results.append({
457
+ "chain": ci["name"],
458
+ "symbol": token["symbol"],
459
+ "address": addr,
460
+ "price_usd": round(price, 6),
461
+ "supply_rate_pct": round(supply_rate, 3),
462
+ "borrow_rate_pct": round(borrow_rate, 3),
463
+ "utilization_pct": round(util, 1),
464
+ "supply_amount": round(supply, 4),
465
+ "supply_usd": round(supply * price),
466
+ "borrow_amount": round(borrow, 4),
467
+ "borrow_usd": round(borrow * price),
468
+ "available_amount": round(supply - borrow, 4),
469
+ "available_usd": round((supply - borrow) * price),
470
+ })
471
+ except Exception as e:
472
+ _err(f"Failed to fetch {ci['name']} markets: {e}")
473
+
474
+ results.sort(key=lambda x: -x["supply_usd"])
475
+ _out({"timestamp": datetime.now(timezone.utc).isoformat(), "count": len(results), "markets": results})
476
+
477
+
478
+ def cmd_account(args):
479
+ """Fetch full position detail for a specific address."""
480
+ address = args.address.lower()
481
+ chains = _resolve_chains(args.chain)
482
+ account_data = {
483
+ "address": address,
484
+ "chains": {},
485
+ "total_supply_usd": 0,
486
+ "total_borrow_usd": 0,
487
+ "total_net_usd": 0,
488
+ }
489
+
490
+ for chain_key in chains:
491
+ ci = CHAINS[chain_key]
492
+ try:
493
+ prices = _api(f"/tokens/{ci['chain_id']}/prices").get("prices", {})
494
+
495
+ # Get all margin accounts for this user
496
+ d = _gql(ci["subgraph"], """
497
+ query($user: String!) {
498
+ marginAccounts(where: {effectiveUser: $user}, first: 50) {
499
+ id
500
+ accountNumber
501
+ lastUpdatedTimestamp
502
+ hasBorrowValue
503
+ tokenValues(first: 30) {
504
+ token { id symbol }
505
+ valuePar
506
+ }
507
+ borrowTokens { symbol }
508
+ supplyTokens { symbol }
509
+ }
510
+ }
511
+ """, {"user": address}, timeout=20)
512
+
513
+ accounts = []
514
+ chain_supply = 0
515
+ chain_borrow = 0
516
+
517
+ for acct in d.get("marginAccounts", []):
518
+ supply_usd = 0
519
+ borrow_usd = 0
520
+ holdings = []
521
+
522
+ for tv in acct["tokenValues"]:
523
+ val = float(tv["valuePar"])
524
+ if abs(val) < 0.0001:
525
+ continue
526
+ price = float(prices.get(tv["token"]["id"], 0))
527
+ usd = val * price
528
+ if val > 0:
529
+ supply_usd += usd
530
+ else:
531
+ borrow_usd += abs(usd)
532
+ holdings.append({
533
+ "token": tv["token"]["symbol"],
534
+ "amount": round(val, 6),
535
+ "usd": round(usd, 2),
536
+ "side": "supply" if val > 0 else "borrow",
537
+ })
538
+
539
+ if not holdings:
540
+ continue
541
+
542
+ ltv = (borrow_usd / supply_usd * 100) if supply_usd > 0 else 0
543
+ chain_supply += supply_usd
544
+ chain_borrow += borrow_usd
545
+
546
+ accounts.append({
547
+ "account_number": acct["accountNumber"],
548
+ "has_borrow": acct["hasBorrowValue"],
549
+ "supply_usd": round(supply_usd),
550
+ "borrow_usd": round(borrow_usd),
551
+ "net_usd": round(supply_usd - borrow_usd),
552
+ "ltv_pct": round(ltv, 1),
553
+ "last_active": int(acct["lastUpdatedTimestamp"]),
554
+ "last_active_time": datetime.fromtimestamp(int(acct["lastUpdatedTimestamp"]), tz=timezone.utc).isoformat(),
555
+ "holdings": sorted(holdings, key=lambda h: -abs(h["usd"])),
556
+ })
557
+
558
+ if accounts:
559
+ account_data["chains"][ci["name"]] = {
560
+ "supply_usd": round(chain_supply),
561
+ "borrow_usd": round(chain_borrow),
562
+ "net_usd": round(chain_supply - chain_borrow),
563
+ "accounts": sorted(accounts, key=lambda a: -a["supply_usd"]),
564
+ }
565
+ account_data["total_supply_usd"] += round(chain_supply)
566
+ account_data["total_borrow_usd"] += round(chain_borrow)
567
+
568
+ # Also fetch recent activity
569
+ flows = _gql(ci["subgraph"], """
570
+ query($user: String!) {
571
+ deposits(
572
+ where: {marginAccount_: {effectiveUser: $user}}
573
+ orderBy: serialId, orderDirection: desc, first: 10
574
+ ) {
575
+ transaction { timestamp }
576
+ token { symbol }
577
+ amountDeltaWei
578
+ amountUSDDeltaWei
579
+ }
580
+ withdrawals(
581
+ where: {marginAccount_: {effectiveUser: $user}}
582
+ orderBy: serialId, orderDirection: desc, first: 10
583
+ ) {
584
+ transaction { timestamp }
585
+ token { symbol }
586
+ amountDeltaWei
587
+ amountUSDDeltaWei
588
+ }
589
+ }
590
+ """, {"user": address}, timeout=20)
591
+
592
+ recent_activity = []
593
+ for dep in flows.get("deposits", []):
594
+ usd = float(dep["amountUSDDeltaWei"])
595
+ if usd < 100:
596
+ continue
597
+ recent_activity.append({
598
+ "type": "deposit",
599
+ "chain": ci["name"],
600
+ "token": dep["token"]["symbol"],
601
+ "amount": round(float(dep["amountDeltaWei"]), 4),
602
+ "usd": round(usd),
603
+ "timestamp": int(dep["transaction"]["timestamp"]),
604
+ "time": datetime.fromtimestamp(int(dep["transaction"]["timestamp"]), tz=timezone.utc).isoformat(),
605
+ })
606
+ for wd in flows.get("withdrawals", []):
607
+ usd = float(wd["amountUSDDeltaWei"])
608
+ if usd < 100:
609
+ continue
610
+ recent_activity.append({
611
+ "type": "withdrawal",
612
+ "chain": ci["name"],
613
+ "token": wd["token"]["symbol"],
614
+ "amount": round(float(wd["amountDeltaWei"]), 4),
615
+ "usd": round(usd),
616
+ "timestamp": int(wd["transaction"]["timestamp"]),
617
+ "time": datetime.fromtimestamp(int(wd["transaction"]["timestamp"]), tz=timezone.utc).isoformat(),
618
+ })
619
+
620
+ if recent_activity:
621
+ recent_activity.sort(key=lambda x: -x["timestamp"])
622
+ if ci["name"] in account_data["chains"]:
623
+ account_data["chains"][ci["name"]]["recent_activity"] = recent_activity
624
+
625
+ except Exception as e:
626
+ _err(f"Failed to fetch {ci['name']} account: {e}")
627
+
628
+ account_data["total_net_usd"] = account_data["total_supply_usd"] - account_data["total_borrow_usd"]
629
+ account_data["timestamp"] = datetime.now(timezone.utc).isoformat()
630
+ account_data["debank_url"] = f"https://debank.com/profile/{address}"
631
+ _out(account_data)
632
+
633
+
634
+ def cmd_risks(args):
635
+ """Identify high-risk positions and dangerous utilization levels."""
636
+ min_ltv = args.min_ltv or 75
637
+ min_usd = args.min_usd or 50000
638
+ risks = {
639
+ "high_ltv_positions": [],
640
+ "high_utilization_markets": [],
641
+ }
642
+
643
+ # Get positions across all chains
644
+ for chain_key in ACTIVE_CHAINS:
645
+ ci = CHAINS[chain_key]
646
+ try:
647
+ d = _gql(ci["subgraph"], """{
648
+ marginAccounts(
649
+ where: {hasBorrowValue: true}
650
+ orderBy: lastUpdatedTimestamp
651
+ orderDirection: desc
652
+ first: 200
653
+ ) {
654
+ effectiveUser { id }
655
+ accountNumber
656
+ tokenValues(first: 30) {
657
+ token { symbol id }
658
+ valuePar
659
+ }
660
+ supplyTokens { symbol }
661
+ borrowTokens { symbol }
662
+ }
663
+ }""", timeout=30)
664
+
665
+ prices = _api(f"/tokens/{ci['chain_id']}/prices").get("prices", {})
666
+
667
+ for acct in d.get("marginAccounts", []):
668
+ supply_usd = 0
669
+ borrow_usd = 0
670
+ for tv in acct["tokenValues"]:
671
+ val = float(tv["valuePar"])
672
+ price = float(prices.get(tv["token"]["id"], 0))
673
+ if val > 0:
674
+ supply_usd += val * price
675
+ else:
676
+ borrow_usd += abs(val * price)
677
+
678
+ if supply_usd < min_usd:
679
+ continue
680
+ ltv = (borrow_usd / supply_usd * 100) if supply_usd > 0 else 0
681
+ if ltv >= min_ltv:
682
+ risks["high_ltv_positions"].append({
683
+ "chain": ci["name"],
684
+ "address": acct["effectiveUser"]["id"],
685
+ "supply_usd": round(supply_usd),
686
+ "borrow_usd": round(borrow_usd),
687
+ "ltv_pct": round(ltv, 1),
688
+ "supply_tokens": [t["symbol"] for t in acct["supplyTokens"]],
689
+ "borrow_tokens": [t["symbol"] for t in acct["borrowTokens"]],
690
+ })
691
+ except Exception as e:
692
+ _err(f"Failed to fetch {ci['name']} risk positions: {e}")
693
+
694
+ # Check market utilization
695
+ for chain_key in ACTIVE_CHAINS:
696
+ ci = CHAINS[chain_key]
697
+ try:
698
+ tokens_resp = _api(f"/tokens/{ci['chain_id']}")
699
+ rates_resp = _api(f"/tokens/{ci['chain_id']}/interest-rates")
700
+ prices_resp = _api(f"/tokens/{ci['chain_id']}/prices")
701
+
702
+ tokens = {t["id"]: t for t in tokens_resp.get("tokens", [])}
703
+ rates = {r["token"]["tokenAddress"]: r for r in rates_resp.get("interestRates", [])}
704
+ prices = prices_resp.get("prices", {})
705
+
706
+ for addr, token in tokens.items():
707
+ price = float(prices.get(addr, 0))
708
+ supply = float(token.get("supplyLiquidity", 0))
709
+ borrow = float(token.get("borrowLiquidity", 0))
710
+ supply_usd = supply * price
711
+ if supply_usd < 10000:
712
+ continue
713
+ util = (borrow / supply * 100) if supply > 0 else 0
714
+ if util >= 80:
715
+ rate = rates.get(addr, {})
716
+ risks["high_utilization_markets"].append({
717
+ "chain": ci["name"],
718
+ "symbol": token["symbol"],
719
+ "utilization_pct": round(util, 1),
720
+ "supply_usd": round(supply_usd),
721
+ "borrow_usd": round(borrow * price),
722
+ "available_usd": round((supply - borrow) * price),
723
+ "supply_rate_pct": round(float(rate.get("totalSupplyInterestRate", 0)) * 100, 3),
724
+ "borrow_rate_pct": round(float(rate.get("borrowInterestRate", 0)) * 100, 3),
725
+ })
726
+ except Exception as e:
727
+ _err(f"Failed to fetch {ci['name']} market risks: {e}")
728
+
729
+ risks["high_ltv_positions"].sort(key=lambda x: -x["ltv_pct"])
730
+ risks["high_utilization_markets"].sort(key=lambda x: -x["utilization_pct"])
731
+ risks["timestamp"] = datetime.now(timezone.utc).isoformat()
732
+ risks["thresholds"] = {"min_ltv_pct": min_ltv, "min_position_usd": min_usd, "min_utilization_pct": 80}
733
+ _out(risks)
734
+
735
+
736
+ def cmd_schema(args):
737
+ """Show available subgraph entities and example queries."""
738
+ _out({
739
+ "description": "Dolomite CLI — query Dolomite protocol data across all chains",
740
+ "version": __version__,
741
+ "subgraph_base": SUBGRAPH_BASE,
742
+ "api_base": API_BASE,
743
+ "chains": {k: {"name": v["name"], "chain_id": v["chain_id"], "subgraph": v["subgraph"]} for k, v in CHAINS.items()},
744
+ "active_chains": ACTIVE_CHAINS,
745
+ "commands": {
746
+ "rates": "All markets with supply/borrow rates. Options: --chain, --stables-only, --min-tvl",
747
+ "positions": "Top borrowing positions. Options: --chain, --top N, --min-usd N",
748
+ "flows": "Recent large deposits/withdrawals. Options: --chain, --hours N, --min-usd N, --type deposit|withdrawal",
749
+ "liquidations": "Recent liquidation events. Options: --chain, --hours N, --limit N",
750
+ "tvl": "Protocol-level TVL summary per chain",
751
+ "markets": "Detailed info for a specific token. Options: --token SYMBOL, --chain",
752
+ "account": "Full position detail for an address. Args: <address>. Options: --chain",
753
+ "risks": "High-risk positions and dangerous utilization. Options: --min-ltv N, --min-usd N",
754
+ "schema": "This help message with available entities and example queries",
755
+ },
756
+ "key_subgraph_entities": [
757
+ "DolomiteMargin — protocol-level aggregates (TVL, users, volumes)",
758
+ "Token — listed assets with addresses and metadata",
759
+ "MarginAccount — user positions with tokenValues (supply/borrow per asset)",
760
+ "BorrowPosition — individual borrow positions with status",
761
+ "Deposit — deposit events with amounts, timestamps, users",
762
+ "Withdrawal — withdrawal events with amounts, timestamps, users",
763
+ "Liquidation — liquidation events with seized collateral and debt",
764
+ "Trade — trade/swap events within margin accounts",
765
+ "AmmLiquidity* — AMM pool data (AmmLiquidityPosition, AmmLiquiditySnapshot)",
766
+ ],
767
+ "example_queries": {
768
+ "get_all_rates": "dolomite rates",
769
+ "stablecoin_rates_only": "dolomite rates --stables-only",
770
+ "ethereum_positions": "dolomite positions --chain ethereum --top 20",
771
+ "whale_flows_48h": "dolomite flows --hours 48 --min-usd 100000",
772
+ "deposits_only": "dolomite flows --type deposit --hours 72",
773
+ "specific_token": "dolomite markets --token USDC",
774
+ "lookup_whale": "dolomite account 0x8be46b25d59616e594f0a9e20147fb14c1b989d9",
775
+ "high_risk": "dolomite risks --min-ltv 80 --min-usd 100000",
776
+ "berachain_only": "dolomite rates --chain berachain",
777
+ },
778
+ "custom_queries": {
779
+ "description": "The CLI commands are convenience wrappers. You can also query the Dolomite subgraphs directly via GraphQL for ANY data not covered by the built-in commands. The subgraphs contain the COMPLETE on-chain history — every deposit, withdrawal, liquidation, trade, position change, and margin account update since deployment. There are NO time limits or data caps.",
780
+ "subgraph_url_pattern": "POST https://subgraph.api.dolomite.io/api/public/1301d2d1-7a9d-4be4-9e9a-061cb8611549/subgraphs/{subgraph_name}/latest/gn",
781
+ "subgraph_names": {
782
+ "ethereum": "dolomite-ethereum",
783
+ "arbitrum": "dolomite-arbitrum",
784
+ "berachain": "dolomite-berachain-mainnet",
785
+ "mantle": "dolomite-mantle",
786
+ "base": "dolomite-base",
787
+ "xlayer": "dolomite-x-layer"
788
+ },
789
+ "auth": "None required. All endpoints are public.",
790
+ "how_to_query": "Send a POST request with JSON body: {\"query\": \"{ ... }\", \"variables\": {...}}. Returns {\"data\": {...}}.",
791
+ "pagination": "Subgraph max is 1000 items per query. Use 'first: N, skip: M' or cursor-based pagination with 'where: {id_gt: lastId}' for larger datasets.",
792
+ "introspection": "Run this to discover all entity types and fields: {\"query\": \"{__schema{types{name fields{name type{name}}}}}\"}. Or for a single entity: {\"query\": \"{__type(name:\\\"Deposit\\\"){fields{name type{name}}}}\"}",
793
+ "example_custom_queries": {
794
+ "all_deposits_by_address_all_time": "curl -s -X POST '{subgraph_url}' -H 'Content-Type: application/json' -d '{\"query\": \"{deposits(where: {marginAccount_: {effectiveUser: \\\"0xADDRESS\\\"}}, orderBy: serialId, orderDirection: desc, first: 1000) { transaction { timestamp } token { symbol } amountDeltaWei amountUSDDeltaWei }}\"}'",
795
+ "trades_in_last_30_days": "curl -s -X POST '{subgraph_url}' -H 'Content-Type: application/json' -d '{\"query\": \"{trades(where: {transaction_: {timestamp_gte: UNIX_30_DAYS_AGO}}, orderBy: serialId, orderDirection: desc, first: 100) { transaction { timestamp } makerToken { symbol } takerToken { symbol } makerTokenDeltaWei takerTokenDeltaWei }}\"}'",
796
+ "total_protocol_stats": "curl -s -X POST '{subgraph_url}' -H 'Content-Type: application/json' -d '{\"query\": \"{dolomiteMargins(first:1) { supplyLiquidityUSD borrowLiquidityUSD userCount totalTradeVolumeUSD totalLiquidationVolumeUSD liquidationCount borrowPositionCount numberOfMarkets }}\"}'",
797
+ "all_borrow_positions_for_token": "curl -s -X POST '{subgraph_url}' -H 'Content-Type: application/json' -d '{\"query\": \"{borrowPositions(where: {borrowToken_: {symbol: \\\"USDC\\\"}, status: Open}, first: 100, orderBy: amounts__borrowAmountUSD, orderDirection: desc) { effectiveUser { id } amounts { borrowAmountUSD supplyAmountUSD } }}\"}'",
798
+ "margin_account_full_detail": "curl -s -X POST '{subgraph_url}' -H 'Content-Type: application/json' -d '{\"query\": \"{marginAccounts(where: {effectiveUser: \\\"0xADDRESS\\\"}, first: 50) { id accountNumber lastUpdatedTimestamp hasBorrowValue tokenValues(first: 30) { token { symbol id } valuePar } supplyTokens { symbol } borrowTokens { symbol } }}\"}'"
799
+ },
800
+ "rest_api": {
801
+ "description": "Dolomite also has a REST API for real-time prices, interest rates, and token metadata.",
802
+ "base_url": "https://api.dolomite.io",
803
+ "endpoints": {
804
+ "tokens": "/tokens/{chainId} — all listed tokens with supply/borrow amounts",
805
+ "interest_rates": "/tokens/{chainId}/interest-rates — current supply and borrow interest rates",
806
+ "prices": "/tokens/{chainId}/prices — current USD prices for all tokens"
807
+ },
808
+ "chain_ids": {"ethereum": 1, "arbitrum": 42161, "berachain": 80094, "mantle": 5000, "base": 8453, "xlayer": 196}
809
+ }
810
+ },
811
+ })
812
+
813
+
814
+ # ─── CLI Setup ────────────────────────────────────────────────────────────────
815
+
816
+ def main():
817
+ parser = argparse.ArgumentParser(
818
+ prog="dolomite",
819
+ description="Query Dolomite protocol data across all chains. Returns structured JSON.",
820
+ )
821
+ parser.add_argument("--version", action="version", version=f"dolomite {__version__}")
822
+ sub = parser.add_subparsers(dest="command", help="Available commands")
823
+
824
+ # rates
825
+ p = sub.add_parser("rates", help="All markets with supply/borrow rates")
826
+ p.add_argument("--chain", help="Filter by chain (ethereum, arbitrum, berachain, all)")
827
+ p.add_argument("--stables-only", action="store_true", help="Only show stablecoin markets")
828
+ p.add_argument("--min-tvl", type=float, help="Minimum TVL in USD (default: 1000)")
829
+
830
+ # positions
831
+ p = sub.add_parser("positions", help="Top borrowing positions")
832
+ p.add_argument("--chain", help="Filter by chain")
833
+ p.add_argument("--top", type=int, help="Number of positions to return (default: 50)")
834
+ p.add_argument("--min-usd", type=float, help="Minimum position size in USD (default: 10000)")
835
+
836
+ # flows
837
+ p = sub.add_parser("flows", help="Recent large deposits/withdrawals")
838
+ p.add_argument("--chain", help="Filter by chain")
839
+ p.add_argument("--hours", type=int, help="Lookback period in hours (default: 24)")
840
+ p.add_argument("--min-usd", type=float, help="Minimum flow size in USD (default: 50000)")
841
+ p.add_argument("--type", choices=["deposit", "withdrawal"], help="Filter by flow type")
842
+
843
+ # liquidations
844
+ p = sub.add_parser("liquidations", help="Recent liquidation events")
845
+ p.add_argument("--chain", help="Filter by chain")
846
+ p.add_argument("--hours", type=int, help="Lookback period in hours (default: 168 = 7 days)")
847
+ p.add_argument("--limit", type=int, help="Max events to return (default: 100)")
848
+
849
+ # tvl
850
+ p = sub.add_parser("tvl", help="Protocol TVL summary per chain")
851
+ p.add_argument("--chain", help="Filter by chain")
852
+
853
+ # markets
854
+ p = sub.add_parser("markets", help="Detailed info for a specific token")
855
+ p.add_argument("--token", help="Token symbol (e.g., USDC, WETH)")
856
+ p.add_argument("--chain", help="Filter by chain")
857
+
858
+ # account
859
+ p = sub.add_parser("account", help="Full position detail for an address")
860
+ p.add_argument("address", help="Ethereum address (0x...)")
861
+ p.add_argument("--chain", help="Filter by chain")
862
+
863
+ # risks
864
+ p = sub.add_parser("risks", help="High-risk positions and utilization alerts")
865
+ p.add_argument("--min-ltv", type=float, help="Minimum LTV to flag (default: 75)")
866
+ p.add_argument("--min-usd", type=float, help="Minimum position size (default: 50000)")
867
+
868
+ # schema
869
+ sub.add_parser("schema", help="Show available data and example queries")
870
+
871
+ args = parser.parse_args()
872
+ if not args.command:
873
+ parser.print_help()
874
+ sys.exit(1)
875
+
876
+ cmd_map = {
877
+ "rates": cmd_rates,
878
+ "positions": cmd_positions,
879
+ "flows": cmd_flows,
880
+ "liquidations": cmd_liquidations,
881
+ "tvl": cmd_tvl,
882
+ "markets": cmd_markets,
883
+ "account": cmd_account,
884
+ "risks": cmd_risks,
885
+ "schema": cmd_schema,
886
+ }
887
+ cmd_map[args.command](args)
888
+
889
+
890
+ if __name__ == "__main__":
891
+ main()
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "dolomite-cli"
7
+ version = "1.0.0"
8
+ description = "CLI tool for querying Dolomite protocol data across all chains"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ keywords = ["dolomite", "defi", "cli", "blockchain", "interest-rates"]
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Environment :: Console",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Topic :: Office/Business :: Financial",
24
+ ]
25
+
26
+ [project.scripts]
27
+ dolomite = "dolomite_cli:main"
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/openclaw/dolomite-cli"