mev-bot-watcher 1.7.2__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.
- mev_bot_watcher-1.7.2/LICENSE +21 -0
- mev_bot_watcher-1.7.2/PKG-INFO +226 -0
- mev_bot_watcher-1.7.2/README.md +202 -0
- mev_bot_watcher-1.7.2/mev_bot_watcher/__init__.py +0 -0
- mev_bot_watcher-1.7.2/mev_bot_watcher/__main__.py +3 -0
- mev_bot_watcher-1.7.2/mev_bot_watcher/coingecko_client.py +119 -0
- mev_bot_watcher-1.7.2/mev_bot_watcher/eth_client.py +116 -0
- mev_bot_watcher-1.7.2/mev_bot_watcher/log_progress.py +56 -0
- mev_bot_watcher-1.7.2/mev_bot_watcher/main.py +222 -0
- mev_bot_watcher-1.7.2/mev_bot_watcher/telegram_notifier.py +404 -0
- mev_bot_watcher-1.7.2/mev_bot_watcher/tx_analyzer.py +341 -0
- mev_bot_watcher-1.7.2/mev_bot_watcher/tx_watcher.py +50 -0
- mev_bot_watcher-1.7.2/mev_bot_watcher/ws_connector.py +136 -0
- mev_bot_watcher-1.7.2/mev_bot_watcher.egg-info/PKG-INFO +226 -0
- mev_bot_watcher-1.7.2/mev_bot_watcher.egg-info/SOURCES.txt +22 -0
- mev_bot_watcher-1.7.2/mev_bot_watcher.egg-info/dependency_links.txt +1 -0
- mev_bot_watcher-1.7.2/mev_bot_watcher.egg-info/entry_points.txt +2 -0
- mev_bot_watcher-1.7.2/mev_bot_watcher.egg-info/requires.txt +5 -0
- mev_bot_watcher-1.7.2/mev_bot_watcher.egg-info/top_level.txt +1 -0
- mev_bot_watcher-1.7.2/pyproject.toml +38 -0
- mev_bot_watcher-1.7.2/setup.cfg +4 -0
- mev_bot_watcher-1.7.2/tests/test_telegram_notifier.py +363 -0
- mev_bot_watcher-1.7.2/tests/test_tx_analyzer.py +587 -0
- mev_bot_watcher-1.7.2/tests/test_ws_connector.py +97 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Nov1kov
|
|
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,226 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mev-bot-watcher
|
|
3
|
+
Version: 1.7.2
|
|
4
|
+
Summary: CLI tool for analyzing and realtime monitoring of MEV bot profitability on EVM-compatible networks
|
|
5
|
+
Author-email: Nov1kov <spellh1@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Nov1kov/mev-bot-watcher
|
|
8
|
+
Project-URL: Issues, https://github.com/Nov1kov/mev-bot-watcher/issues
|
|
9
|
+
Keywords: mev,ethereum,evm,arbitrage,monitoring,pnl
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
15
|
+
Requires-Python: >=3.10
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: PyYAML>=6.0
|
|
19
|
+
Requires-Dist: aiohttp>=3.14
|
|
20
|
+
Requires-Dist: websockets>=16.0
|
|
21
|
+
Requires-Dist: click>=8.1
|
|
22
|
+
Requires-Dist: croniter>=6.0
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
|
|
25
|
+
# MEV Bot Watcher
|
|
26
|
+
|
|
27
|
+
[](https://github.com/Nov1kov/mev-bot-watcher/actions/workflows/tests.yml)
|
|
28
|
+
[](https://pypi.org/project/mev-bot-watcher/)
|
|
29
|
+
[](https://hub.docker.com/r/nov1kov/mev-watcher)
|
|
30
|
+
[](LICENSE)
|
|
31
|
+
|
|
32
|
+
[Русская версия](README.ru.md)
|
|
33
|
+
|
|
34
|
+
CLI tool for analyzing and realtime monitoring of MEV bot profitability on EVM-compatible networks (Ethereum, Arbitrum, etc.).
|
|
35
|
+
|
|
36
|
+
Scans historical blocks or subscribes to new ones via WebSocket, finds transactions of a watched address, parses ERC20 Transfer (and WETH Deposit/Withdrawal) events for a set of tracked tokens and calculates P&L converted to USD: `Σ(net per token × price) − gas spent in the native token × native price`.
|
|
37
|
+
|
|
38
|
+
## Features
|
|
39
|
+
|
|
40
|
+
- **Multi-token P&L in USD** — track the wrapped native token plus any number of `base_tokens` (e.g. USDC/USDT/DAI for stablecoin arbitrage); each token's balance change is valued at its own price and decimals, gas is valued via the native token
|
|
41
|
+
- **Retrospective analysis** — scan a range of blocks, calculate profit per block and total summary
|
|
42
|
+
- **Realtime monitoring** — subscribe to new blocks via WebSocket
|
|
43
|
+
- **Multichain** — multiple networks via config (Ethereum, Arbitrum, etc.)
|
|
44
|
+
- **Telegram notifications** — aggregated reports with configurable interval, USD profit and per-token balances; bot addresses can link to a block explorer via `scanner_url`
|
|
45
|
+
- **Instant loss alerts** — optional per-network `loss_alert_usd`: as soon as a block turns out unprofitable, a separate message is sent with links to the offending transactions
|
|
46
|
+
|
|
47
|
+
## Configuration
|
|
48
|
+
|
|
49
|
+
Copy `config.example.yaml` to `config.yaml` and fill in your values:
|
|
50
|
+
|
|
51
|
+
```yaml
|
|
52
|
+
telegram:
|
|
53
|
+
bot_token: 'YOUR_BOT_TOKEN'
|
|
54
|
+
chat_id: 'YOUR_CHAT_ID'
|
|
55
|
+
notify_schedule: '0 * * * *' # cron syntax (every hour)
|
|
56
|
+
|
|
57
|
+
bots:
|
|
58
|
+
ethereum:
|
|
59
|
+
# wrapped_token — the wrapped native token (WETH). Its price values the gas
|
|
60
|
+
# spent, and it is also counted as a profit token.
|
|
61
|
+
wrapped_token: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'
|
|
62
|
+
# base_tokens (optional) — extra tokens whose balance change is included in
|
|
63
|
+
# the profit (e.g. stablecoins for arbitrage). Each may have its own decimals.
|
|
64
|
+
base_tokens:
|
|
65
|
+
- '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' # USDC
|
|
66
|
+
- '0xdAC17F958D2ee523a2206206994597C13D831ec7' # USDT
|
|
67
|
+
watched_address: '0xYOUR_BOT_ADDRESS'
|
|
68
|
+
# scanner_url (optional) — base URL of the network block explorer. Used to
|
|
69
|
+
# turn the bot address in Telegram notifications into a clickable link.
|
|
70
|
+
# Examples: ethereum — https://etherscan.io/, arbitrum — https://arbiscan.io/
|
|
71
|
+
scanner_url: 'https://etherscan.io/'
|
|
72
|
+
# loss_alert_usd (optional) — send an instant alert as soon as a block's P&L
|
|
73
|
+
# in this network is below -$loss_alert_usd. Omit to disable, 0 for any loss.
|
|
74
|
+
loss_alert_usd: 1.0
|
|
75
|
+
http_rpc_url: 'https://your-rpc-provider.com/api-key'
|
|
76
|
+
ws_rpc_url: 'wss://your-rpc-provider.com/api-key'
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Token `symbol`/`decimals` are read from each contract over RPC, and the USD price
|
|
80
|
+
is auto-resolved via CoinGecko by symbol. For tokens that are not the wrapped
|
|
81
|
+
native token, `base_tokens` is optional — omit it for single-token bots.
|
|
82
|
+
|
|
83
|
+
`scanner_url` is optional too: when set, the bot address in Telegram messages
|
|
84
|
+
becomes a clickable link to its page on the block explorer (e.g.
|
|
85
|
+
`https://etherscan.io/address/0x...`); when omitted, the address is shown as
|
|
86
|
+
plain monospace text.
|
|
87
|
+
|
|
88
|
+
### WebSocket with Basic Auth
|
|
89
|
+
|
|
90
|
+
If your WebSocket endpoint requires HTTP Basic authentication, pass `ws_rpc_url`
|
|
91
|
+
as a nested block with `url`, `login` and `password` fields. Plain string form
|
|
92
|
+
stays supported for endpoints without auth.
|
|
93
|
+
|
|
94
|
+
```yaml
|
|
95
|
+
bots:
|
|
96
|
+
my_node:
|
|
97
|
+
wrapped_token: '0x...'
|
|
98
|
+
watched_address: '0xYOUR_BOT_ADDRESS'
|
|
99
|
+
http_rpc_url: 'http://user:pass@your-node-ip:8549'
|
|
100
|
+
ws_rpc_url:
|
|
101
|
+
url: 'ws://your-node-ip:8549'
|
|
102
|
+
login: 'your_login'
|
|
103
|
+
password: 'your_password'
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Telegram notifications
|
|
107
|
+
|
|
108
|
+
Startup message. Per-token balances are fetched via RPC (native `eth_getBalance` + `balanceOf` for each tracked token); USD prices are auto-resolved via CoinGecko:
|
|
109
|
+
```
|
|
110
|
+
🚀 MEV Monitor Started
|
|
111
|
+
|
|
112
|
+
• ethereum (WETH — $3,210.50)
|
|
113
|
+
0xYOUR_BOT_ADDRESS
|
|
114
|
+
💰 Balance:
|
|
115
|
+
ETH: 1.5000 ($4,815.75)
|
|
116
|
+
USDC: 1000.0000 ($1,000.00)
|
|
117
|
+
|
|
118
|
+
⏰ Schedule: 0 * * * *
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
If a token cannot be resolved on CoinGecko, its USD part is omitted and only the amount is shown.
|
|
122
|
+
|
|
123
|
+
Periodic report. Profit is the multi-token P&L converted to USD (token balance
|
|
124
|
+
changes minus gas), and the current per-token balances are refreshed via RPC at
|
|
125
|
+
send time:
|
|
126
|
+
```
|
|
127
|
+
✅ ETHEREUM
|
|
128
|
+
0x1234...5678
|
|
129
|
+
├ Successful txs: 3/4
|
|
130
|
+
└ Total: $+2.50
|
|
131
|
+
💰 Balance:
|
|
132
|
+
ETH: 1.4980 ($4,809.32)
|
|
133
|
+
USDC: 1001.7200 ($1,001.72)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Instant loss alerts
|
|
137
|
+
|
|
138
|
+
`loss_alert_usd` is set per network, next to that bot's `watched_address` — so
|
|
139
|
+
you can alert on any loss on a cheap L2 and only on large ones on mainnet, or
|
|
140
|
+
leave it out entirely for networks you don't want alerts for.
|
|
141
|
+
|
|
142
|
+
When it is set, every block containing transactions of the watched address is
|
|
143
|
+
priced right after it is read. If its P&L (token balance changes minus gas) is
|
|
144
|
+
below `-loss_alert_usd`, a separate message is sent immediately — without
|
|
145
|
+
waiting for the next scheduled report. The block still counts towards the
|
|
146
|
+
periodic report as usual.
|
|
147
|
+
|
|
148
|
+
Transaction hashes link to the block explorer when `scanner_url` is configured
|
|
149
|
+
(otherwise they are shown as plain monospace text). A block may contain several
|
|
150
|
+
transactions of the bot — all of them are listed:
|
|
151
|
+
```
|
|
152
|
+
🚨 LOSS — ETHEREUM $-10.12
|
|
153
|
+
0x1234...5678
|
|
154
|
+
├ Block: 21500123
|
|
155
|
+
├ Txs: 2 (failed: 1)
|
|
156
|
+
├ WETH: -0.004200 ($-10.50)
|
|
157
|
+
├ USDC: +1.500000 ($+1.50)
|
|
158
|
+
└ Gas: 0.001500 ETH ($3.75)
|
|
159
|
+
🔗 0xaa1111...111111, 0xbb2222...222222
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`loss_alert_usd: 0` alerts on any loss; a higher value (e.g. `5`) filters out
|
|
163
|
+
noise from cheap failed transactions. USD prices are cached for a minute, so
|
|
164
|
+
frequent blocks do not hammer the CoinGecko API.
|
|
165
|
+
|
|
166
|
+
## Usage
|
|
167
|
+
|
|
168
|
+
### Docker
|
|
169
|
+
|
|
170
|
+
Monitor all bots:
|
|
171
|
+
```bash
|
|
172
|
+
docker run -d -v ./config.yaml:/app/config.yaml nov1kov/mev-watcher
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Docker Compose
|
|
176
|
+
|
|
177
|
+
Create `docker-compose.yml` next to your `config.yaml`:
|
|
178
|
+
|
|
179
|
+
```yaml
|
|
180
|
+
services:
|
|
181
|
+
mev-watcher:
|
|
182
|
+
image: nov1kov/mev-watcher
|
|
183
|
+
restart: unless-stopped
|
|
184
|
+
volumes:
|
|
185
|
+
- ./config.yaml:/app/config.yaml
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### pip
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
pip install mev-bot-watcher
|
|
192
|
+
mev-watcher monitor
|
|
193
|
+
mev-watcher analyze -b ethereum -s 18000000
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### From source
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
pip install .
|
|
200
|
+
python -m mev_bot_watcher monitor
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Commands
|
|
204
|
+
|
|
205
|
+
`monitor` — subscribe to new blocks via WebSocket:
|
|
206
|
+
- `-b, --bot-name <name>` — specific bot (optional; all bots if omitted)
|
|
207
|
+
|
|
208
|
+
`analyze` — retrospective analysis. Exactly one of the block options is required:
|
|
209
|
+
- `-b, --bot-name <name>` — bot to analyze (required)
|
|
210
|
+
- `-s, --start-block <N>` — scan from block N to the latest
|
|
211
|
+
- `-n, --block <N>` — prefetch and analyze a single block (debug mode)
|
|
212
|
+
|
|
213
|
+
`-c, --config <path>` — path to config (default `config.yaml`) for both commands.
|
|
214
|
+
|
|
215
|
+
Examples:
|
|
216
|
+
```bash
|
|
217
|
+
mev-watcher monitor -b ethereum
|
|
218
|
+
mev-watcher analyze -b ethereum -s 18000000
|
|
219
|
+
mev-watcher analyze -b ethereum -n 18500000
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Tests
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
python -m unittest discover tests
|
|
226
|
+
```
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# MEV Bot Watcher
|
|
2
|
+
|
|
3
|
+
[](https://github.com/Nov1kov/mev-bot-watcher/actions/workflows/tests.yml)
|
|
4
|
+
[](https://pypi.org/project/mev-bot-watcher/)
|
|
5
|
+
[](https://hub.docker.com/r/nov1kov/mev-watcher)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
[Русская версия](README.ru.md)
|
|
9
|
+
|
|
10
|
+
CLI tool for analyzing and realtime monitoring of MEV bot profitability on EVM-compatible networks (Ethereum, Arbitrum, etc.).
|
|
11
|
+
|
|
12
|
+
Scans historical blocks or subscribes to new ones via WebSocket, finds transactions of a watched address, parses ERC20 Transfer (and WETH Deposit/Withdrawal) events for a set of tracked tokens and calculates P&L converted to USD: `Σ(net per token × price) − gas spent in the native token × native price`.
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- **Multi-token P&L in USD** — track the wrapped native token plus any number of `base_tokens` (e.g. USDC/USDT/DAI for stablecoin arbitrage); each token's balance change is valued at its own price and decimals, gas is valued via the native token
|
|
17
|
+
- **Retrospective analysis** — scan a range of blocks, calculate profit per block and total summary
|
|
18
|
+
- **Realtime monitoring** — subscribe to new blocks via WebSocket
|
|
19
|
+
- **Multichain** — multiple networks via config (Ethereum, Arbitrum, etc.)
|
|
20
|
+
- **Telegram notifications** — aggregated reports with configurable interval, USD profit and per-token balances; bot addresses can link to a block explorer via `scanner_url`
|
|
21
|
+
- **Instant loss alerts** — optional per-network `loss_alert_usd`: as soon as a block turns out unprofitable, a separate message is sent with links to the offending transactions
|
|
22
|
+
|
|
23
|
+
## Configuration
|
|
24
|
+
|
|
25
|
+
Copy `config.example.yaml` to `config.yaml` and fill in your values:
|
|
26
|
+
|
|
27
|
+
```yaml
|
|
28
|
+
telegram:
|
|
29
|
+
bot_token: 'YOUR_BOT_TOKEN'
|
|
30
|
+
chat_id: 'YOUR_CHAT_ID'
|
|
31
|
+
notify_schedule: '0 * * * *' # cron syntax (every hour)
|
|
32
|
+
|
|
33
|
+
bots:
|
|
34
|
+
ethereum:
|
|
35
|
+
# wrapped_token — the wrapped native token (WETH). Its price values the gas
|
|
36
|
+
# spent, and it is also counted as a profit token.
|
|
37
|
+
wrapped_token: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'
|
|
38
|
+
# base_tokens (optional) — extra tokens whose balance change is included in
|
|
39
|
+
# the profit (e.g. stablecoins for arbitrage). Each may have its own decimals.
|
|
40
|
+
base_tokens:
|
|
41
|
+
- '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' # USDC
|
|
42
|
+
- '0xdAC17F958D2ee523a2206206994597C13D831ec7' # USDT
|
|
43
|
+
watched_address: '0xYOUR_BOT_ADDRESS'
|
|
44
|
+
# scanner_url (optional) — base URL of the network block explorer. Used to
|
|
45
|
+
# turn the bot address in Telegram notifications into a clickable link.
|
|
46
|
+
# Examples: ethereum — https://etherscan.io/, arbitrum — https://arbiscan.io/
|
|
47
|
+
scanner_url: 'https://etherscan.io/'
|
|
48
|
+
# loss_alert_usd (optional) — send an instant alert as soon as a block's P&L
|
|
49
|
+
# in this network is below -$loss_alert_usd. Omit to disable, 0 for any loss.
|
|
50
|
+
loss_alert_usd: 1.0
|
|
51
|
+
http_rpc_url: 'https://your-rpc-provider.com/api-key'
|
|
52
|
+
ws_rpc_url: 'wss://your-rpc-provider.com/api-key'
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Token `symbol`/`decimals` are read from each contract over RPC, and the USD price
|
|
56
|
+
is auto-resolved via CoinGecko by symbol. For tokens that are not the wrapped
|
|
57
|
+
native token, `base_tokens` is optional — omit it for single-token bots.
|
|
58
|
+
|
|
59
|
+
`scanner_url` is optional too: when set, the bot address in Telegram messages
|
|
60
|
+
becomes a clickable link to its page on the block explorer (e.g.
|
|
61
|
+
`https://etherscan.io/address/0x...`); when omitted, the address is shown as
|
|
62
|
+
plain monospace text.
|
|
63
|
+
|
|
64
|
+
### WebSocket with Basic Auth
|
|
65
|
+
|
|
66
|
+
If your WebSocket endpoint requires HTTP Basic authentication, pass `ws_rpc_url`
|
|
67
|
+
as a nested block with `url`, `login` and `password` fields. Plain string form
|
|
68
|
+
stays supported for endpoints without auth.
|
|
69
|
+
|
|
70
|
+
```yaml
|
|
71
|
+
bots:
|
|
72
|
+
my_node:
|
|
73
|
+
wrapped_token: '0x...'
|
|
74
|
+
watched_address: '0xYOUR_BOT_ADDRESS'
|
|
75
|
+
http_rpc_url: 'http://user:pass@your-node-ip:8549'
|
|
76
|
+
ws_rpc_url:
|
|
77
|
+
url: 'ws://your-node-ip:8549'
|
|
78
|
+
login: 'your_login'
|
|
79
|
+
password: 'your_password'
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Telegram notifications
|
|
83
|
+
|
|
84
|
+
Startup message. Per-token balances are fetched via RPC (native `eth_getBalance` + `balanceOf` for each tracked token); USD prices are auto-resolved via CoinGecko:
|
|
85
|
+
```
|
|
86
|
+
🚀 MEV Monitor Started
|
|
87
|
+
|
|
88
|
+
• ethereum (WETH — $3,210.50)
|
|
89
|
+
0xYOUR_BOT_ADDRESS
|
|
90
|
+
💰 Balance:
|
|
91
|
+
ETH: 1.5000 ($4,815.75)
|
|
92
|
+
USDC: 1000.0000 ($1,000.00)
|
|
93
|
+
|
|
94
|
+
⏰ Schedule: 0 * * * *
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
If a token cannot be resolved on CoinGecko, its USD part is omitted and only the amount is shown.
|
|
98
|
+
|
|
99
|
+
Periodic report. Profit is the multi-token P&L converted to USD (token balance
|
|
100
|
+
changes minus gas), and the current per-token balances are refreshed via RPC at
|
|
101
|
+
send time:
|
|
102
|
+
```
|
|
103
|
+
✅ ETHEREUM
|
|
104
|
+
0x1234...5678
|
|
105
|
+
├ Successful txs: 3/4
|
|
106
|
+
└ Total: $+2.50
|
|
107
|
+
💰 Balance:
|
|
108
|
+
ETH: 1.4980 ($4,809.32)
|
|
109
|
+
USDC: 1001.7200 ($1,001.72)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Instant loss alerts
|
|
113
|
+
|
|
114
|
+
`loss_alert_usd` is set per network, next to that bot's `watched_address` — so
|
|
115
|
+
you can alert on any loss on a cheap L2 and only on large ones on mainnet, or
|
|
116
|
+
leave it out entirely for networks you don't want alerts for.
|
|
117
|
+
|
|
118
|
+
When it is set, every block containing transactions of the watched address is
|
|
119
|
+
priced right after it is read. If its P&L (token balance changes minus gas) is
|
|
120
|
+
below `-loss_alert_usd`, a separate message is sent immediately — without
|
|
121
|
+
waiting for the next scheduled report. The block still counts towards the
|
|
122
|
+
periodic report as usual.
|
|
123
|
+
|
|
124
|
+
Transaction hashes link to the block explorer when `scanner_url` is configured
|
|
125
|
+
(otherwise they are shown as plain monospace text). A block may contain several
|
|
126
|
+
transactions of the bot — all of them are listed:
|
|
127
|
+
```
|
|
128
|
+
🚨 LOSS — ETHEREUM $-10.12
|
|
129
|
+
0x1234...5678
|
|
130
|
+
├ Block: 21500123
|
|
131
|
+
├ Txs: 2 (failed: 1)
|
|
132
|
+
├ WETH: -0.004200 ($-10.50)
|
|
133
|
+
├ USDC: +1.500000 ($+1.50)
|
|
134
|
+
└ Gas: 0.001500 ETH ($3.75)
|
|
135
|
+
🔗 0xaa1111...111111, 0xbb2222...222222
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`loss_alert_usd: 0` alerts on any loss; a higher value (e.g. `5`) filters out
|
|
139
|
+
noise from cheap failed transactions. USD prices are cached for a minute, so
|
|
140
|
+
frequent blocks do not hammer the CoinGecko API.
|
|
141
|
+
|
|
142
|
+
## Usage
|
|
143
|
+
|
|
144
|
+
### Docker
|
|
145
|
+
|
|
146
|
+
Monitor all bots:
|
|
147
|
+
```bash
|
|
148
|
+
docker run -d -v ./config.yaml:/app/config.yaml nov1kov/mev-watcher
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Docker Compose
|
|
152
|
+
|
|
153
|
+
Create `docker-compose.yml` next to your `config.yaml`:
|
|
154
|
+
|
|
155
|
+
```yaml
|
|
156
|
+
services:
|
|
157
|
+
mev-watcher:
|
|
158
|
+
image: nov1kov/mev-watcher
|
|
159
|
+
restart: unless-stopped
|
|
160
|
+
volumes:
|
|
161
|
+
- ./config.yaml:/app/config.yaml
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### pip
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
pip install mev-bot-watcher
|
|
168
|
+
mev-watcher monitor
|
|
169
|
+
mev-watcher analyze -b ethereum -s 18000000
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### From source
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
pip install .
|
|
176
|
+
python -m mev_bot_watcher monitor
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Commands
|
|
180
|
+
|
|
181
|
+
`monitor` — subscribe to new blocks via WebSocket:
|
|
182
|
+
- `-b, --bot-name <name>` — specific bot (optional; all bots if omitted)
|
|
183
|
+
|
|
184
|
+
`analyze` — retrospective analysis. Exactly one of the block options is required:
|
|
185
|
+
- `-b, --bot-name <name>` — bot to analyze (required)
|
|
186
|
+
- `-s, --start-block <N>` — scan from block N to the latest
|
|
187
|
+
- `-n, --block <N>` — prefetch and analyze a single block (debug mode)
|
|
188
|
+
|
|
189
|
+
`-c, --config <path>` — path to config (default `config.yaml`) for both commands.
|
|
190
|
+
|
|
191
|
+
Examples:
|
|
192
|
+
```bash
|
|
193
|
+
mev-watcher monitor -b ethereum
|
|
194
|
+
mev-watcher analyze -b ethereum -s 18000000
|
|
195
|
+
mev-watcher analyze -b ethereum -n 18500000
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Tests
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
python -m unittest discover tests
|
|
202
|
+
```
|
|
File without changes
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Optional, Dict, List, Iterable
|
|
3
|
+
|
|
4
|
+
import aiohttp
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
COINGECKO_BASE_URL = "https://api.coingecko.com/api/v3"
|
|
8
|
+
|
|
9
|
+
# Оверрайды для популярных токенов. Нужны для разрешения неоднозначности
|
|
10
|
+
# (у одного тикера может быть много coin id в CoinGecko). Покрывают как
|
|
11
|
+
# нативные токены, так и распространённые стейблкоины (base_tokens).
|
|
12
|
+
_KNOWN_IDS = {
|
|
13
|
+
# native
|
|
14
|
+
"eth": "ethereum",
|
|
15
|
+
"btc": "bitcoin",
|
|
16
|
+
"bnb": "binancecoin",
|
|
17
|
+
"matic": "matic-network",
|
|
18
|
+
"pol": "polygon-ecosystem-token",
|
|
19
|
+
"sol": "solana",
|
|
20
|
+
"avax": "avalanche-2",
|
|
21
|
+
"ftm": "fantom",
|
|
22
|
+
"trx": "tron",
|
|
23
|
+
"near": "near",
|
|
24
|
+
"ada": "cardano",
|
|
25
|
+
"dot": "polkadot",
|
|
26
|
+
"mon": "monad",
|
|
27
|
+
# stablecoins / common base tokens
|
|
28
|
+
"usdc": "usd-coin",
|
|
29
|
+
"usdt": "tether",
|
|
30
|
+
"usde": "ethena-usde",
|
|
31
|
+
"dai": "dai",
|
|
32
|
+
"usds": "usds",
|
|
33
|
+
"frax": "frax",
|
|
34
|
+
"tusd": "true-usd",
|
|
35
|
+
"busd": "binance-usd",
|
|
36
|
+
"usdc.e": "usd-coin",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class CoinGeckoClient:
|
|
41
|
+
"""Клиент CoinGecko: резолвит coin id по тикеру и получает цены в USD."""
|
|
42
|
+
|
|
43
|
+
def __init__(self):
|
|
44
|
+
self._session: Optional[aiohttp.ClientSession] = None
|
|
45
|
+
self._symbol_to_ids: Optional[Dict[str, List[str]]] = None
|
|
46
|
+
|
|
47
|
+
async def __aenter__(self):
|
|
48
|
+
self._session = aiohttp.ClientSession()
|
|
49
|
+
return self
|
|
50
|
+
|
|
51
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
52
|
+
if self._session:
|
|
53
|
+
await self._session.close()
|
|
54
|
+
self._session = None
|
|
55
|
+
|
|
56
|
+
async def _ensure_coins_list(self):
|
|
57
|
+
"""Одноразовая загрузка списка монет CoinGecko с кешем на процесс."""
|
|
58
|
+
if self._symbol_to_ids is not None:
|
|
59
|
+
return
|
|
60
|
+
url = f"{COINGECKO_BASE_URL}/coins/list"
|
|
61
|
+
async with self._session.get(url) as resp:
|
|
62
|
+
coins = await resp.json()
|
|
63
|
+
mapping: Dict[str, List[str]] = {}
|
|
64
|
+
for coin in coins:
|
|
65
|
+
sym = (coin.get("symbol") or "").lower()
|
|
66
|
+
if not sym:
|
|
67
|
+
continue
|
|
68
|
+
mapping.setdefault(sym, []).append(coin["id"])
|
|
69
|
+
self._symbol_to_ids = mapping
|
|
70
|
+
|
|
71
|
+
async def resolve_id_by_symbol(self, symbol: str) -> Optional[str]:
|
|
72
|
+
"""Подбор coingecko id по тикеру.
|
|
73
|
+
|
|
74
|
+
Для wrapped-обёрток (символ начинается с W) первым делом пробуем
|
|
75
|
+
нативный эквивалент: WETH->ETH, WMON->MON и т.д. — это корректно,
|
|
76
|
+
т.к. цены wrapped-токена и нативного равны по смыслу 1:1. Если
|
|
77
|
+
нативный не находится уверенно, падаем обратно к точному поиску
|
|
78
|
+
исходного символа.
|
|
79
|
+
"""
|
|
80
|
+
await self._ensure_coins_list()
|
|
81
|
+
sym = symbol.lower()
|
|
82
|
+
|
|
83
|
+
if sym.startswith("w") and len(sym) > 1:
|
|
84
|
+
unwrapped = self._confident_lookup(sym[1:])
|
|
85
|
+
if unwrapped:
|
|
86
|
+
return unwrapped
|
|
87
|
+
|
|
88
|
+
return self._best_effort_lookup(sym)
|
|
89
|
+
|
|
90
|
+
def _confident_lookup(self, sym: str) -> Optional[str]:
|
|
91
|
+
"""Возвращает id только если уверены: либо оверрайд, либо id == sym."""
|
|
92
|
+
if sym in _KNOWN_IDS:
|
|
93
|
+
return _KNOWN_IDS[sym]
|
|
94
|
+
for cid in self._symbol_to_ids.get(sym) or []:
|
|
95
|
+
if cid == sym:
|
|
96
|
+
return cid
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
def _best_effort_lookup(self, sym: str) -> Optional[str]:
|
|
100
|
+
"""Точное совпадение по тикеру, даже если однозначности нет."""
|
|
101
|
+
confident = self._confident_lookup(sym)
|
|
102
|
+
if confident:
|
|
103
|
+
return confident
|
|
104
|
+
candidates = self._symbol_to_ids.get(sym) or []
|
|
105
|
+
return candidates[0] if candidates else None
|
|
106
|
+
|
|
107
|
+
async def get_prices_usd(self, ids: Iterable[str]) -> Dict[str, float]:
|
|
108
|
+
"""Получение цен сразу пачкой (CoinGecko поддерживает ids через запятую)."""
|
|
109
|
+
unique = sorted({cid for cid in ids if cid})
|
|
110
|
+
if not unique:
|
|
111
|
+
return {}
|
|
112
|
+
url = f"{COINGECKO_BASE_URL}/simple/price?ids={','.join(unique)}&vs_currencies=usd"
|
|
113
|
+
async with self._session.get(url) as resp:
|
|
114
|
+
data = await resp.json()
|
|
115
|
+
return {cid: payload["usd"] for cid, payload in data.items() if "usd" in payload}
|
|
116
|
+
|
|
117
|
+
async def get_price_usd(self, coingecko_id: str) -> Optional[float]:
|
|
118
|
+
prices = await self.get_prices_usd([coingecko_id])
|
|
119
|
+
return prices.get(coingecko_id)
|