multi-chain-balance-diff 0.1.0

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 ADDED
@@ -0,0 +1,332 @@
1
+ # multi-chain-balance-diff
2
+
3
+ [![Tests](https://github.com/metawake/multi-chain-balance-diff/actions/workflows/test.yml/badge.svg)](https://github.com/metawake/multi-chain-balance-diff/actions/workflows/test.yml)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ **Compare wallet balances across EVM and Solana chains. Returns structured diffs.**
7
+
8
+ ```bash
9
+ mcbd --address 0x... --network mainnet --json | jq '.native.diff'
10
+ ```
11
+
12
+ ## What
13
+
14
+ CLI that fetches balance at block N and block N-50, computes the difference, outputs JSON or pretty-prints. Supports Ethereum, Polygon, Base, Arbitrum, Optimism, Solana, Helium.
15
+
16
+ ## Who
17
+
18
+ Infrastructure engineers, protocol developers, and operators who need scriptable balance checks without spinning up dashboards or paying for analytics platforms.
19
+
20
+ ## Why
21
+
22
+ Portfolio trackers are UI-only. Analytics platforms are expensive and slow. Raw RPC calls require boilerplate. This fills the gap: a `diff`-like primitive for on-chain balances that fits into shell scripts, cron jobs, and CI pipelines.
23
+
24
+ ---
25
+
26
+ ## Use Cases
27
+
28
+ ### 1. Operations: Treasury monitoring
29
+
30
+ ```bash
31
+ # Cron job: alert ops channel if treasury balance drops
32
+ mcbd --address 0xTreasury --network mainnet --json \
33
+ | jq -e '.native.diffSign == "negative"' \
34
+ && curl -X POST $SLACK_WEBHOOK -d '{"text":"Treasury balance decreased"}'
35
+ ```
36
+
37
+ ### 2. CI/Monitoring: Post-deployment sanity check
38
+
39
+ ```bash
40
+ # GitHub Actions: fail pipeline if fee wallet drained unexpectedly
41
+ mcbd --address $FEE_WALLET --network base --alert-if-diff "<-0.1"
42
+ # exit 0 = OK, exit 1 = threshold breached, exit 2 = RPC error
43
+ ```
44
+
45
+ ### 3. DeFi/Rewards: Staking reward accrual verification
46
+
47
+ ```bash
48
+ # Verify staking rewards are accruing over 1000 blocks
49
+ mcbd --address $STAKER --network mainnet --blocks 1000 --json \
50
+ | jq '.native.diff'
51
+ # Expected: positive value if rewards distributed
52
+ ```
53
+
54
+ ---
55
+
56
+ ## Install
57
+
58
+ ```bash
59
+ npm install -g multi-chain-balance-diff
60
+ ```
61
+
62
+ **One-shot (no install):**
63
+
64
+ ```bash
65
+ npx multi-chain-balance-diff -a 0x... -n mainnet --json
66
+ ```
67
+
68
+ ---
69
+
70
+ ## Quickstart (60 seconds)
71
+
72
+ ```bash
73
+ # 1. Check a wallet balance diff (uses public RPC)
74
+ mcbd --address 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 --network mainnet
75
+
76
+ # 2. Get JSON output, extract the diff
77
+ mcbd -a 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 -n mainnet --json | jq '.native.diff'
78
+
79
+ # 3. CI check: exit 1 if balance dropped more than 0.1 ETH
80
+ mcbd -a 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 -n mainnet --alert-if-diff "<-0.1"
81
+ echo "Exit code: $?"
82
+ # 0 = OK, 1 = threshold triggered, 2 = RPC error
83
+ ```
84
+
85
+ **One-liner for npx (no install):**
86
+
87
+ ```bash
88
+ npx multi-chain-balance-diff -a 0x... -n mainnet --json
89
+ ```
90
+
91
+ ---
92
+
93
+ ## Usage
94
+
95
+ ```bash
96
+ mcbd --address 0x... --network mainnet # pretty output
97
+ mcbd --address 0x... --json # structured output
98
+ mcbd --address 0x... --blocks 1000 # custom lookback
99
+ mcbd --address 0x... --watch --interval 30 # continuous monitoring
100
+ mcbd --addresses 0xA,0xB,0xC --network base # batch
101
+ mcbd --address 0x... --alert-if-diff ">0.01" # CI threshold
102
+ ```
103
+
104
+ ## Networks
105
+
106
+ | Key | Type | Native | Tokens |
107
+ |-----|------|--------|--------|
108
+ | `mainnet` | EVM | ETH | USDC, USDT, stETH |
109
+ | `polygon` | EVM | MATIC | USDC, USDT, WETH |
110
+ | `base` | EVM | ETH | USDC, cbETH, DAI |
111
+ | `arbitrum` | EVM | ETH | USDC, ARB, GMX |
112
+ | `optimism` | EVM | ETH | USDC, OP, SNX |
113
+ | `solana` | Solana | SOL | USDC, BONK, JUP |
114
+ | `helium` | Solana | SOL | HNT, MOBILE, IOT, DC |
115
+
116
+ ## Options
117
+
118
+ | Flag | Description |
119
+ |------|-------------|
120
+ | `-a, --address` | Wallet address |
121
+ | `-A, --addresses` | Multiple addresses (comma-sep or file path) |
122
+ | `-n, --network` | Target network (default: `mainnet`) |
123
+ | `-b, --blocks` | Lookback depth (default: `50`) |
124
+ | `-w, --watch` | Continuous monitoring mode |
125
+ | `-i, --interval` | Watch interval in seconds (default: `30`) |
126
+ | `-c, --count` | Exit after N polls (watch mode) |
127
+ | `--exit-on-error` | Exit immediately on RPC failure (watch mode) |
128
+ | `--exit-on-diff` | Exit immediately when threshold triggers (watch mode) |
129
+ | `--json` | JSON output |
130
+ | `--no-tokens` | Skip ERC-20/SPL token checks |
131
+ | `--alert-if-diff` | Exit 1 if diff matches condition (e.g., `">0.01"`, `"<-1"`) |
132
+ | `--alert-pct` | Exit 1 if diff exceeds % of balance (e.g., `">5"`, `"<-10"`) |
133
+
134
+ **Exit codes:** `0` OK · `1` diff triggered · `2` RPC failure · `130` SIGINT
135
+
136
+ ---
137
+
138
+ ## Exit Codes & Batch Semantics
139
+
140
+ | Code | Meaning | When |
141
+ |------|---------|------|
142
+ | `0` | OK | No threshold triggered, all queries succeeded |
143
+ | `1` | Diff triggered | Threshold condition matched (any address) |
144
+ | `2` | RPC error | Network/connection failure |
145
+ | `130` | SIGINT | User interrupted (Ctrl+C) |
146
+
147
+ **Batch mode (`--addresses`)**: Returns exit `1` if *any* address triggers the threshold. Partial failures are included in JSON output with `error` field per address; successful queries still return data.
148
+
149
+ ---
150
+
151
+ ## Common Failure Modes
152
+
153
+ ### RPC Rate Limits
154
+
155
+ Public RPCs have strict rate limits. Symptoms: `429 Too Many Requests` or slow responses.
156
+
157
+ ```bash
158
+ # Solution: Use a private RPC (Alchemy, Infura, QuickNode)
159
+ export ETH_RPC_URL=https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY
160
+ ```
161
+
162
+ ### Unavailable Chain / RPC Down
163
+
164
+ ```json
165
+ {"schemaVersion":"0.1.0","error":"connect ECONNREFUSED 127.0.0.1:8545","code":"ECONNREFUSED","exitCode":2}
166
+ ```
167
+
168
+ Exit code `2` indicates RPC failure. Check network connectivity and RPC URL validity.
169
+
170
+ ### Invalid Address Format
171
+
172
+ ```bash
173
+ $ mcbd --address invalid --network mainnet --json
174
+ {"schemaVersion":"0.1.0","error":"Invalid EVM address: invalid"}
175
+ ```
176
+
177
+ EVM addresses must be `0x` + 40 hex chars. Solana addresses are base58 encoded.
178
+
179
+ ### Missing Environment Variables
180
+
181
+ If using custom RPC endpoints via env vars, ensure they're set before running:
182
+
183
+ ```bash
184
+ # Check if RPC env is set
185
+ [ -z "$ETH_RPC_URL" ] && echo "Warning: Using public RPC (rate limited)"
186
+ ```
187
+
188
+ ---
189
+
190
+ ## Watch Mode for CI/Cron
191
+
192
+ Watch mode is designed for long-running monitoring, cron jobs, and CI pipelines.
193
+
194
+ ### Patterns
195
+
196
+ ```bash
197
+ # One-shot: poll once, exit on threshold
198
+ mcbd -a $ADDR -n base --count 1 --alert-if-diff ">0.01" --json
199
+
200
+ # CI health check: 5 polls, fail fast on error or diff
201
+ mcbd -a $ADDR -n mainnet --watch --count 5 \
202
+ --exit-on-error --exit-on-diff --alert-pct ">10" --json
203
+
204
+ # Cron monitor: 10 polls over 5 minutes, log NDJSON
205
+ mcbd -a $ADDR -n solana --watch --interval 30 --count 10 --json >> /var/log/balance.ndjson
206
+
207
+ # Infinite watch with alerts (SIGINT to stop)
208
+ mcbd -a $ADDR -n polygon --watch --interval 60 --alert-if-diff "<-1"
209
+ ```
210
+
211
+ ### Example Output
212
+
213
+ **Normal operation (no diff):**
214
+
215
+ ```json
216
+ {
217
+ "schemaVersion": "0.1.0",
218
+ "network": { "key": "mainnet", "name": "Ethereum Mainnet", "chainType": "evm", "chainId": 1 },
219
+ "address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
220
+ "explorer": "https://etherscan.io/address/0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
221
+ "block": { "current": 19234567, "previous": 19234517 },
222
+ "native": {
223
+ "symbol": "ETH",
224
+ "decimals": 18,
225
+ "balance": "1.234567",
226
+ "balanceRaw": "1234567000000000000",
227
+ "diff": "0",
228
+ "diffRaw": "0",
229
+ "diffSign": "positive"
230
+ },
231
+ "tokens": [],
232
+ "timestamp": "2025-01-15T10:30:00.000Z"
233
+ }
234
+ ```
235
+
236
+ Exit: `0`
237
+
238
+ **Diff detected (threshold triggered):**
239
+
240
+ ```json
241
+ {
242
+ "schemaVersion": "0.1.0",
243
+ "network": { "key": "base", "name": "Base", "chainType": "evm", "chainId": 8453 },
244
+ "address": "0xTreasury...",
245
+ "block": { "current": 8765432, "previous": 8765382 },
246
+ "native": {
247
+ "symbol": "ETH",
248
+ "balance": "10.5",
249
+ "diff": "0.05",
250
+ "diffRaw": "50000000000000000",
251
+ "diffSign": "positive"
252
+ },
253
+ "alert": {
254
+ "threshold": ">0.01",
255
+ "thresholdPct": null,
256
+ "triggered": true,
257
+ "triggeredBy": "absolute"
258
+ },
259
+ "timestamp": "2025-01-15T10:30:00.000Z"
260
+ }
261
+ ```
262
+
263
+ Exit: `1`
264
+
265
+ **Error state (RPC unavailable):**
266
+
267
+ ```json
268
+ {
269
+ "schemaVersion": "0.1.0",
270
+ "error": "connect ECONNREFUSED 127.0.0.1:8545",
271
+ "code": "ECONNREFUSED",
272
+ "exitCode": 2
273
+ }
274
+ ```
275
+
276
+ Exit: `2`
277
+
278
+ **Watch mode NDJSON stream:**
279
+
280
+ ```json
281
+ {"schemaVersion":"0.1.0","type":"watch_start","timestamp":"2025-01-15T10:30:00.000Z","network":"mainnet","address":"0x...","interval":30,"count":3}
282
+ {"schemaVersion":"0.1.0","timestamp":"2025-01-15T10:30:00.500Z","address":"0x...","block":19234567,"balance":"1.234","diff":"0","alert":false,"poll":1}
283
+ {"schemaVersion":"0.1.0","timestamp":"2025-01-15T10:30:30.500Z","address":"0x...","block":19234569,"balance":"1.234","diff":"0","alert":false,"poll":2}
284
+ {"schemaVersion":"0.1.0","timestamp":"2025-01-15T10:31:00.500Z","address":"0x...","block":19234571,"balance":"1.235","diff":"0.001","alert":true,"poll":3}
285
+ {"schemaVersion":"0.1.0","type":"watch_end","timestamp":"2025-01-15T10:31:00.600Z","polls":3,"exitCode":1}
286
+ ```
287
+
288
+ ---
289
+
290
+ ## Extending
291
+
292
+ ```
293
+ src/
294
+ ├── index.js # CLI
295
+ ├── config/networks.js # Chain definitions
296
+ ├── adapters/
297
+ │ ├── baseAdapter.js # Interface
298
+ │ ├── evmAdapter.js # ethers.js
299
+ │ └── solanaAdapter.js # @solana/web3.js
300
+ ```
301
+
302
+ Add a chain: implement `BaseAdapter`, add to `networks.js`, register in `adapters/index.js`.
303
+
304
+ ## JSON Schema
305
+
306
+ JSON output includes a `schemaVersion` field (e.g., `"0.1.0"`). Schema changes are versioned:
307
+
308
+ - **Patch**: Documentation, new optional fields
309
+ - **Minor**: New output types, additive changes
310
+ - **Major**: Breaking changes to existing fields
311
+
312
+ See [`schema/mcbd-output.schema.json`](./schema/mcbd-output.schema.json) for the full specification.
313
+
314
+ ## Stability
315
+
316
+ - JSON schema is versioned. Breaking changes = major version bump.
317
+ - Adapter interface is stable. New chains can be added without breaking existing integrations.
318
+ - See [CHANGELOG.md](./CHANGELOG.md) for release history.
319
+
320
+ ## License
321
+
322
+ MIT
323
+
324
+ ---
325
+
326
+ <!--
327
+ GitHub repo description (1 sentence):
328
+ CLI tool to fetch and diff wallet balances across EVM and Solana chains. JSON output, CI-friendly exit codes.
329
+
330
+ GitHub topics:
331
+ ethereum, solana, blockchain, cli, devops, monitoring, web3, defi
332
+ -->
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "multi-chain-balance-diff",
3
+ "version": "0.1.0",
4
+ "description": "CLI tool to fetch and compare wallet balances across EVM and Solana chains",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "multi-chain-balance-diff": "./src/index.js",
8
+ "mcbd": "./src/index.js",
9
+ "balance-diff": "./src/index.js"
10
+ },
11
+ "files": [
12
+ "src/",
13
+ "schema/"
14
+ ],
15
+ "scripts": {
16
+ "start": "node src/index.js",
17
+ "test": "node --test 'tests/*.test.js'"
18
+ },
19
+ "keywords": [
20
+ "ethereum",
21
+ "solana",
22
+ "web3",
23
+ "balance",
24
+ "multi-chain",
25
+ "cli",
26
+ "staking",
27
+ "defi",
28
+ "helium",
29
+ "crypto",
30
+ "wallet"
31
+ ],
32
+ "author": "",
33
+ "license": "MIT",
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/metawake/multi-chain-balance-diff.git"
37
+ },
38
+ "bugs": {
39
+ "url": "https://github.com/metawake/multi-chain-balance-diff/issues"
40
+ },
41
+ "homepage": "https://github.com/metawake/multi-chain-balance-diff#readme",
42
+ "engines": {
43
+ "node": ">=18.0.0"
44
+ },
45
+ "dependencies": {
46
+ "@solana/web3.js": "^1.95.0",
47
+ "commander": "^12.1.0",
48
+ "dotenv": "^16.4.5",
49
+ "ethers": "^6.13.4"
50
+ }
51
+ }
52
+
@@ -0,0 +1,297 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://github.com/metawake/multi-chain-balance-diff/schema/mcbd-output.schema.json",
4
+ "title": "multi-chain-balance-diff Output Schema",
5
+ "description": "JSON output schema for the mcbd CLI tool",
6
+
7
+ "oneOf": [
8
+ { "$ref": "#/definitions/BalanceResult" },
9
+ { "$ref": "#/definitions/MultiAddressResult" },
10
+ { "$ref": "#/definitions/ErrorResult" },
11
+ { "$ref": "#/definitions/WatchStart" },
12
+ { "$ref": "#/definitions/WatchPoll" },
13
+ { "$ref": "#/definitions/WatchEnd" }
14
+ ],
15
+
16
+ "definitions": {
17
+ "BalanceResult": {
18
+ "type": "object",
19
+ "description": "Single address balance diff result",
20
+ "required": ["schemaVersion", "network", "address", "native", "timestamp"],
21
+ "properties": {
22
+ "schemaVersion": {
23
+ "type": "string",
24
+ "description": "Schema version (semver)",
25
+ "pattern": "^\\d+\\.\\d+\\.\\d+$"
26
+ },
27
+ "network": { "$ref": "#/definitions/NetworkInfo" },
28
+ "address": {
29
+ "type": "string",
30
+ "description": "Wallet address"
31
+ },
32
+ "explorer": {
33
+ "type": "string",
34
+ "format": "uri",
35
+ "description": "Block explorer URL for address"
36
+ },
37
+ "block": { "$ref": "#/definitions/BlockRange" },
38
+ "slot": { "$ref": "#/definitions/BlockRange" },
39
+ "native": { "$ref": "#/definitions/NativeBalance" },
40
+ "tokens": {
41
+ "type": "array",
42
+ "items": { "$ref": "#/definitions/TokenBalance" }
43
+ },
44
+ "alert": { "$ref": "#/definitions/AlertInfo" },
45
+ "timestamp": {
46
+ "type": "string",
47
+ "format": "date-time"
48
+ }
49
+ }
50
+ },
51
+
52
+ "MultiAddressResult": {
53
+ "type": "object",
54
+ "description": "Multi-address batch result",
55
+ "required": ["schemaVersion", "network", "addresses", "summary", "timestamp"],
56
+ "properties": {
57
+ "schemaVersion": {
58
+ "type": "string",
59
+ "pattern": "^\\d+\\.\\d+\\.\\d+$"
60
+ },
61
+ "network": { "$ref": "#/definitions/NetworkInfo" },
62
+ "addresses": {
63
+ "type": "array",
64
+ "items": { "$ref": "#/definitions/BalanceResult" }
65
+ },
66
+ "summary": {
67
+ "type": "object",
68
+ "properties": {
69
+ "totalAddresses": { "type": "integer" },
70
+ "successCount": { "type": "integer" },
71
+ "errorCount": { "type": "integer" }
72
+ },
73
+ "required": ["totalAddresses", "successCount", "errorCount"]
74
+ },
75
+ "alert": { "$ref": "#/definitions/AlertInfo" },
76
+ "timestamp": {
77
+ "type": "string",
78
+ "format": "date-time"
79
+ }
80
+ }
81
+ },
82
+
83
+ "ErrorResult": {
84
+ "type": "object",
85
+ "description": "Error response",
86
+ "required": ["schemaVersion", "error"],
87
+ "properties": {
88
+ "schemaVersion": {
89
+ "type": "string",
90
+ "pattern": "^\\d+\\.\\d+\\.\\d+$"
91
+ },
92
+ "error": {
93
+ "type": "string",
94
+ "description": "Error message"
95
+ },
96
+ "code": {
97
+ "type": ["string", "null"],
98
+ "description": "Error code (e.g., ECONNREFUSED)"
99
+ },
100
+ "exitCode": {
101
+ "type": "integer",
102
+ "description": "Process exit code"
103
+ }
104
+ }
105
+ },
106
+
107
+ "WatchStart": {
108
+ "type": "object",
109
+ "description": "Watch mode start event (NDJSON)",
110
+ "required": ["schemaVersion", "type", "timestamp", "network", "address", "interval"],
111
+ "properties": {
112
+ "schemaVersion": {
113
+ "type": "string",
114
+ "pattern": "^\\d+\\.\\d+\\.\\d+$"
115
+ },
116
+ "type": {
117
+ "const": "watch_start"
118
+ },
119
+ "timestamp": {
120
+ "type": "string",
121
+ "format": "date-time"
122
+ },
123
+ "network": { "type": "string" },
124
+ "address": { "type": "string" },
125
+ "interval": { "type": "integer" },
126
+ "count": { "type": ["integer", "null"] },
127
+ "threshold": { "type": ["string", "null"] },
128
+ "thresholdPct": { "type": ["string", "null"] }
129
+ }
130
+ },
131
+
132
+ "WatchPoll": {
133
+ "type": "object",
134
+ "description": "Watch mode poll event (NDJSON)",
135
+ "required": ["schemaVersion", "timestamp", "address"],
136
+ "properties": {
137
+ "schemaVersion": {
138
+ "type": "string",
139
+ "pattern": "^\\d+\\.\\d+\\.\\d+$"
140
+ },
141
+ "timestamp": {
142
+ "type": "string",
143
+ "format": "date-time"
144
+ },
145
+ "address": { "type": "string" },
146
+ "block": { "type": "integer" },
147
+ "slot": { "type": "integer" },
148
+ "balance": { "type": "string" },
149
+ "balanceRaw": { "type": "string" },
150
+ "diff": { "type": "string" },
151
+ "diffRaw": { "type": "string" },
152
+ "diffSign": {
153
+ "type": "string",
154
+ "enum": ["positive", "negative"]
155
+ },
156
+ "alert": { "type": "boolean" },
157
+ "poll": { "type": "integer" },
158
+ "error": { "type": "string" },
159
+ "isRpcError": { "type": "boolean" }
160
+ }
161
+ },
162
+
163
+ "WatchEnd": {
164
+ "type": "object",
165
+ "description": "Watch mode end event (NDJSON)",
166
+ "required": ["schemaVersion", "type", "timestamp", "polls", "exitCode"],
167
+ "properties": {
168
+ "schemaVersion": {
169
+ "type": "string",
170
+ "pattern": "^\\d+\\.\\d+\\.\\d+$"
171
+ },
172
+ "type": {
173
+ "const": "watch_end"
174
+ },
175
+ "timestamp": {
176
+ "type": "string",
177
+ "format": "date-time"
178
+ },
179
+ "polls": { "type": "integer" },
180
+ "exitCode": { "type": "integer" },
181
+ "reason": { "type": "string" }
182
+ }
183
+ },
184
+
185
+ "NetworkInfo": {
186
+ "type": "object",
187
+ "required": ["key", "name", "chainType"],
188
+ "properties": {
189
+ "key": {
190
+ "type": "string",
191
+ "description": "Network identifier (e.g., mainnet, polygon, solana)"
192
+ },
193
+ "name": {
194
+ "type": "string",
195
+ "description": "Human-readable network name"
196
+ },
197
+ "chainType": {
198
+ "type": "string",
199
+ "enum": ["evm", "solana"]
200
+ },
201
+ "chainId": {
202
+ "type": "integer",
203
+ "description": "EVM chain ID (null for Solana)"
204
+ }
205
+ }
206
+ },
207
+
208
+ "BlockRange": {
209
+ "type": "object",
210
+ "required": ["current", "previous"],
211
+ "properties": {
212
+ "current": {
213
+ "type": "integer",
214
+ "description": "Current block/slot number"
215
+ },
216
+ "previous": {
217
+ "type": "integer",
218
+ "description": "Previous block/slot for diff comparison"
219
+ }
220
+ }
221
+ },
222
+
223
+ "NativeBalance": {
224
+ "type": "object",
225
+ "required": ["symbol", "decimals", "balance", "balanceRaw", "diff", "diffRaw", "diffSign"],
226
+ "properties": {
227
+ "symbol": {
228
+ "type": "string",
229
+ "description": "Native token symbol (ETH, MATIC, SOL, etc.)"
230
+ },
231
+ "decimals": {
232
+ "type": "integer",
233
+ "description": "Token decimals"
234
+ },
235
+ "balance": {
236
+ "type": "string",
237
+ "description": "Formatted current balance"
238
+ },
239
+ "balanceRaw": {
240
+ "type": "string",
241
+ "description": "Raw balance in smallest unit (wei, lamports, etc.)"
242
+ },
243
+ "diff": {
244
+ "type": "string",
245
+ "description": "Absolute value of balance difference (formatted)"
246
+ },
247
+ "diffRaw": {
248
+ "type": "string",
249
+ "description": "Raw diff value (signed)"
250
+ },
251
+ "diffSign": {
252
+ "type": "string",
253
+ "enum": ["positive", "negative"]
254
+ }
255
+ }
256
+ },
257
+
258
+ "TokenBalance": {
259
+ "type": "object",
260
+ "required": ["symbol", "decimals", "balance", "balanceRaw"],
261
+ "properties": {
262
+ "symbol": { "type": "string" },
263
+ "address": {
264
+ "type": "string",
265
+ "description": "Token contract address (EVM) or mint (Solana)"
266
+ },
267
+ "decimals": { "type": "integer" },
268
+ "balance": { "type": "string" },
269
+ "balanceRaw": { "type": "string" }
270
+ }
271
+ },
272
+
273
+ "AlertInfo": {
274
+ "type": "object",
275
+ "properties": {
276
+ "threshold": {
277
+ "type": ["string", "null"],
278
+ "description": "Absolute threshold (e.g., >0.01)"
279
+ },
280
+ "thresholdPct": {
281
+ "type": ["string", "null"],
282
+ "description": "Percentage threshold (e.g., >5)"
283
+ },
284
+ "triggered": {
285
+ "type": "boolean",
286
+ "description": "Whether alert condition was met"
287
+ },
288
+ "triggeredBy": {
289
+ "type": ["string", "null"],
290
+ "enum": ["absolute", "percentage", null]
291
+ }
292
+ },
293
+ "required": ["triggered"]
294
+ }
295
+ }
296
+ }
297
+