nansen-cli 1.0.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/LICENSE +21 -0
- package/README.md +217 -0
- package/package.json +47 -0
- package/src/api.js +593 -0
- package/src/index.js +404 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nansen
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# Nansen CLI
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/nansen-cli)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[]()
|
|
6
|
+
|
|
7
|
+
Command-line interface for the [Nansen API](https://docs.nansen.ai). Designed for AI agents with structured JSON output.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Install globally via npm
|
|
13
|
+
npm install -g nansen-cli
|
|
14
|
+
|
|
15
|
+
# Or run directly with npx
|
|
16
|
+
npx nansen-cli help
|
|
17
|
+
|
|
18
|
+
# Or clone and install locally
|
|
19
|
+
git clone https://github.com/nansen-ai/nansen-cli.git
|
|
20
|
+
cd nansen-cli
|
|
21
|
+
npm install
|
|
22
|
+
npm link
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Configuration
|
|
26
|
+
|
|
27
|
+
**Option 1: Interactive login (recommended)**
|
|
28
|
+
```bash
|
|
29
|
+
nansen login
|
|
30
|
+
# Enter your API key when prompted
|
|
31
|
+
# ✓ Saved to ~/.nansen/config.json
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Option 2: Environment variable**
|
|
35
|
+
```bash
|
|
36
|
+
export NANSEN_API_KEY=your-api-key
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Get your API key at [app.nansen.ai/api](https://app.nansen.ai/api).
|
|
40
|
+
|
|
41
|
+
## Quick Start
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Get trending tokens on Solana
|
|
45
|
+
nansen token screener --chain solana --timeframe 24h --pretty
|
|
46
|
+
|
|
47
|
+
# Check Smart Money activity
|
|
48
|
+
nansen smart-money netflow --chain solana --pretty
|
|
49
|
+
|
|
50
|
+
# Profile a wallet
|
|
51
|
+
nansen profiler balance --address 0x28c6c06298d514db089934071355e5743bf21d60 --chain ethereum --pretty
|
|
52
|
+
|
|
53
|
+
# Search for an entity
|
|
54
|
+
nansen profiler search --query "Vitalik Buterin" --pretty
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Commands
|
|
58
|
+
|
|
59
|
+
### `smart-money` - Smart Money Analytics
|
|
60
|
+
|
|
61
|
+
Track trading and holding activity of sophisticated market participants.
|
|
62
|
+
|
|
63
|
+
| Subcommand | Description |
|
|
64
|
+
|------------|-------------|
|
|
65
|
+
| `netflow` | Net capital flows (inflows vs outflows) |
|
|
66
|
+
| `dex-trades` | Real-time DEX trading activity |
|
|
67
|
+
| `perp-trades` | Perpetual trading on Hyperliquid |
|
|
68
|
+
| `holdings` | Aggregated token balances |
|
|
69
|
+
| `dcas` | DCA strategies on Jupiter |
|
|
70
|
+
| `historical-holdings` | Historical holdings over time |
|
|
71
|
+
|
|
72
|
+
**Smart Money Labels:**
|
|
73
|
+
- `Fund` - Institutional investment funds
|
|
74
|
+
- `Smart Trader` - Historically profitable traders
|
|
75
|
+
- `30D Smart Trader` - Top performers (30-day window)
|
|
76
|
+
- `90D Smart Trader` - Top performers (90-day window)
|
|
77
|
+
- `180D Smart Trader` - Top performers (180-day window)
|
|
78
|
+
- `Smart HL Perps Trader` - Profitable Hyperliquid traders
|
|
79
|
+
|
|
80
|
+
### `profiler` - Wallet Profiling
|
|
81
|
+
|
|
82
|
+
Detailed information about any blockchain address.
|
|
83
|
+
|
|
84
|
+
| Subcommand | Description |
|
|
85
|
+
|------------|-------------|
|
|
86
|
+
| `balance` | Current token holdings |
|
|
87
|
+
| `labels` | Behavioral and entity labels |
|
|
88
|
+
| `transactions` | Transaction history |
|
|
89
|
+
| `pnl` | PnL and trade performance |
|
|
90
|
+
| `search` | Search for entities by name |
|
|
91
|
+
| `historical-balances` | Historical balances over time |
|
|
92
|
+
| `related-wallets` | Find wallets related to an address |
|
|
93
|
+
| `counterparties` | Top counterparties by volume |
|
|
94
|
+
| `pnl-summary` | Summarized PnL metrics |
|
|
95
|
+
| `perp-positions` | Current perpetual positions |
|
|
96
|
+
| `perp-trades` | Perpetual trading history |
|
|
97
|
+
|
|
98
|
+
### `token` - Token God Mode
|
|
99
|
+
|
|
100
|
+
Deep analytics for any token.
|
|
101
|
+
|
|
102
|
+
| Subcommand | Description |
|
|
103
|
+
|------------|-------------|
|
|
104
|
+
| `screener` | Discover and filter tokens |
|
|
105
|
+
| `holders` | Token holder analysis |
|
|
106
|
+
| `flows` | Token flow metrics |
|
|
107
|
+
| `dex-trades` | DEX trading activity |
|
|
108
|
+
| `pnl` | PnL leaderboard |
|
|
109
|
+
| `who-bought-sold` | Recent buyers and sellers |
|
|
110
|
+
| `flow-intelligence` | Detailed flow intelligence by label |
|
|
111
|
+
| `transfers` | Token transfer history |
|
|
112
|
+
| `jup-dca` | Jupiter DCA orders for token |
|
|
113
|
+
| `perp-trades` | Perp trades by token symbol |
|
|
114
|
+
| `perp-positions` | Open perp positions by token symbol |
|
|
115
|
+
| `perp-pnl-leaderboard` | Perp PnL leaderboard by token |
|
|
116
|
+
|
|
117
|
+
### `portfolio` - Portfolio Analytics
|
|
118
|
+
|
|
119
|
+
| Subcommand | Description |
|
|
120
|
+
|------------|-------------|
|
|
121
|
+
| `defi` | DeFi holdings across protocols |
|
|
122
|
+
|
|
123
|
+
## Options
|
|
124
|
+
|
|
125
|
+
| Option | Description |
|
|
126
|
+
|--------|-------------|
|
|
127
|
+
| `--pretty` | Format JSON output for readability |
|
|
128
|
+
| `--chain <chain>` | Blockchain to query |
|
|
129
|
+
| `--chains <json>` | Multiple chains as JSON array |
|
|
130
|
+
| `--limit <n>` | Number of results |
|
|
131
|
+
| `--days <n>` | Date range in days (default: 30) |
|
|
132
|
+
| `--symbol <sym>` | Token symbol for perp endpoints (e.g., BTC, ETH) |
|
|
133
|
+
| `--filters <json>` | Filter criteria as JSON |
|
|
134
|
+
| `--order-by <json>` | Sort order as JSON array |
|
|
135
|
+
| `--labels <label>` | Smart Money label filter |
|
|
136
|
+
| `--smart-money` | Filter for Smart Money only |
|
|
137
|
+
| `--timeframe <tf>` | Time window (5m, 10m, 1h, 6h, 24h, 7d, 30d) |
|
|
138
|
+
|
|
139
|
+
## Supported Chains
|
|
140
|
+
|
|
141
|
+
`ethereum`, `solana`, `base`, `bnb`, `arbitrum`, `polygon`, `optimism`, `avalanche`, `linea`, `scroll`, `zksync`, `mantle`, `ronin`, `sei`, `plasma`, `sonic`, `unichain`, `monad`, `hyperevm`, `iotaevm`
|
|
142
|
+
|
|
143
|
+
## AI Agent Integration
|
|
144
|
+
|
|
145
|
+
The CLI is designed for AI agents and automation:
|
|
146
|
+
|
|
147
|
+
- **Structured Output**: All responses are JSON with consistent schema
|
|
148
|
+
- **Error Handling**: Errors include status codes and actionable details
|
|
149
|
+
- **Composable**: Commands can be chained with shell pipes
|
|
150
|
+
- **Discoverable**: `help` commands at every level
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
// Success response
|
|
154
|
+
{
|
|
155
|
+
"success": true,
|
|
156
|
+
"data": {
|
|
157
|
+
"results": [...],
|
|
158
|
+
"pagination": {...}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Error response
|
|
163
|
+
{
|
|
164
|
+
"success": false,
|
|
165
|
+
"error": "API error message",
|
|
166
|
+
"status": 401,
|
|
167
|
+
"details": {...}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Examples
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
# Get Smart Money DEX trades from Funds only
|
|
175
|
+
nansen smart-money dex-trades --chain ethereum --labels Fund
|
|
176
|
+
|
|
177
|
+
# Get token holders with Smart Money filter
|
|
178
|
+
nansen token holders --token So11111111111111111111111111111111111111112 --chain solana --smart-money
|
|
179
|
+
|
|
180
|
+
# Get historical holdings for the past 7 days
|
|
181
|
+
nansen smart-money historical-holdings --chain solana --days 7
|
|
182
|
+
|
|
183
|
+
# Get BTC perpetual positions on Hyperliquid
|
|
184
|
+
nansen token perp-positions --symbol BTC --pretty
|
|
185
|
+
|
|
186
|
+
# Get top PnL traders for a token
|
|
187
|
+
nansen token pnl --token JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN --chain solana --days 30
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Development
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
# Run tests (mocked, no API key needed)
|
|
194
|
+
npm test
|
|
195
|
+
|
|
196
|
+
# Run with coverage
|
|
197
|
+
npm run test:coverage
|
|
198
|
+
|
|
199
|
+
# Run against live API
|
|
200
|
+
NANSEN_API_KEY=your-key npm run test:live
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for development guidelines.
|
|
204
|
+
|
|
205
|
+
## API Coverage
|
|
206
|
+
|
|
207
|
+
| Category | Endpoints | Coverage |
|
|
208
|
+
|----------|-----------|----------|
|
|
209
|
+
| Smart Money | 6 | 100% |
|
|
210
|
+
| Profiler | 11 | 100% |
|
|
211
|
+
| Token God Mode | 12 | 100% |
|
|
212
|
+
| Portfolio | 1 | 100% |
|
|
213
|
+
| **Total** | **30** | **100%** |
|
|
214
|
+
|
|
215
|
+
## License
|
|
216
|
+
|
|
217
|
+
[MIT](LICENSE) © [Nansen](https://nansen.ai)
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nansen-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Command-line interface for Nansen API - designed for AI agents",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"nansen": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node src/index.js",
|
|
12
|
+
"test": "vitest run",
|
|
13
|
+
"test:watch": "vitest",
|
|
14
|
+
"test:coverage": "vitest run --coverage",
|
|
15
|
+
"test:live": "NANSEN_LIVE_TEST=1 vitest run"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"nansen",
|
|
19
|
+
"crypto",
|
|
20
|
+
"blockchain",
|
|
21
|
+
"api",
|
|
22
|
+
"cli",
|
|
23
|
+
"ai-agent",
|
|
24
|
+
"smart-money",
|
|
25
|
+
"onchain",
|
|
26
|
+
"defi",
|
|
27
|
+
"solana",
|
|
28
|
+
"ethereum"
|
|
29
|
+
],
|
|
30
|
+
"author": "Nansen <dev@nansen.ai>",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/nansen-ai/nansen-cli.git"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/nansen-ai/nansen-cli/issues"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/nansen-ai/nansen-cli#readme",
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18.0.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
45
|
+
"vitest": "^4.0.18"
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/api.js
ADDED
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nansen API Client
|
|
3
|
+
* Handles all HTTP communication with the Nansen API
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
|
|
10
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
|
|
12
|
+
// ============= Config Paths =============
|
|
13
|
+
|
|
14
|
+
const CONFIG_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '', '.nansen');
|
|
15
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get the config directory path
|
|
19
|
+
*/
|
|
20
|
+
export function getConfigDir() {
|
|
21
|
+
return CONFIG_DIR;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get the config file path
|
|
26
|
+
*/
|
|
27
|
+
export function getConfigFile() {
|
|
28
|
+
return CONFIG_FILE;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Save config to ~/.nansen/config.json
|
|
33
|
+
*/
|
|
34
|
+
export function saveConfig(config) {
|
|
35
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
36
|
+
fs.mkdirSync(CONFIG_DIR, { mode: 0o700, recursive: true });
|
|
37
|
+
}
|
|
38
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Delete config file (logout)
|
|
43
|
+
*/
|
|
44
|
+
export function deleteConfig() {
|
|
45
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
46
|
+
fs.unlinkSync(CONFIG_FILE);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ============= Address Validation =============
|
|
53
|
+
|
|
54
|
+
const ADDRESS_PATTERNS = {
|
|
55
|
+
// EVM chains: 0x followed by 40 hex chars
|
|
56
|
+
evm: /^0x[a-fA-F0-9]{40}$/,
|
|
57
|
+
// Solana: Base58, 32-44 chars (no 0, O, I, l)
|
|
58
|
+
solana: /^[1-9A-HJ-NP-Za-km-z]{32,44}$/,
|
|
59
|
+
// Bitcoin: Various formats
|
|
60
|
+
bitcoin: /^(1|3|bc1)[a-zA-HJ-NP-Z0-9]{25,62}$/,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const EVM_CHAINS = [
|
|
64
|
+
'ethereum', 'arbitrum', 'base', 'bnb', 'polygon', 'optimism',
|
|
65
|
+
'avalanche', 'linea', 'scroll', 'zksync', 'mantle', 'ronin',
|
|
66
|
+
'sei', 'plasma', 'sonic', 'unichain', 'monad', 'hyperevm', 'iotaevm'
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Validate address format for a given chain
|
|
71
|
+
* @param {string} address - The address to validate
|
|
72
|
+
* @param {string} chain - The blockchain (ethereum, solana, etc.)
|
|
73
|
+
* @returns {{valid: boolean, error?: string}}
|
|
74
|
+
*/
|
|
75
|
+
export function validateAddress(address, chain = 'ethereum') {
|
|
76
|
+
if (!address || typeof address !== 'string') {
|
|
77
|
+
return { valid: false, error: 'Address is required' };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const trimmed = address.trim();
|
|
81
|
+
|
|
82
|
+
if (EVM_CHAINS.includes(chain)) {
|
|
83
|
+
if (!ADDRESS_PATTERNS.evm.test(trimmed)) {
|
|
84
|
+
return { valid: false, error: `Invalid EVM address format. Expected 0x followed by 40 hex characters.` };
|
|
85
|
+
}
|
|
86
|
+
} else if (chain === 'solana') {
|
|
87
|
+
if (!ADDRESS_PATTERNS.solana.test(trimmed)) {
|
|
88
|
+
return { valid: false, error: `Invalid Solana address format. Expected Base58 string (32-44 chars).` };
|
|
89
|
+
}
|
|
90
|
+
} else if (chain === 'bitcoin') {
|
|
91
|
+
if (!ADDRESS_PATTERNS.bitcoin.test(trimmed)) {
|
|
92
|
+
return { valid: false, error: `Invalid Bitcoin address format.` };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// For unknown chains, allow any non-empty string (API will validate)
|
|
96
|
+
|
|
97
|
+
return { valid: true };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Validate token address (same rules as wallet address)
|
|
102
|
+
*/
|
|
103
|
+
export function validateTokenAddress(tokenAddress, chain = 'solana') {
|
|
104
|
+
return validateAddress(tokenAddress, chain);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function loadConfig() {
|
|
108
|
+
// Priority: 1. Environment variables, 2. ~/.nansen/config.json, 3. Local config.json
|
|
109
|
+
|
|
110
|
+
// Check environment variables first (highest priority)
|
|
111
|
+
if (process.env.NANSEN_API_KEY) {
|
|
112
|
+
return {
|
|
113
|
+
apiKey: process.env.NANSEN_API_KEY,
|
|
114
|
+
baseUrl: process.env.NANSEN_BASE_URL || 'https://api.nansen.ai'
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Check ~/.nansen/config.json (from `nansen login`)
|
|
119
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
120
|
+
try {
|
|
121
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
|
|
122
|
+
} catch (e) {
|
|
123
|
+
// Ignore parse errors, continue to next option
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Check local config.json (for development)
|
|
128
|
+
const localConfig = path.join(__dirname, '..', 'config.json');
|
|
129
|
+
if (fs.existsSync(localConfig)) {
|
|
130
|
+
return JSON.parse(fs.readFileSync(localConfig, 'utf8'));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// No config found
|
|
134
|
+
return {
|
|
135
|
+
apiKey: null,
|
|
136
|
+
baseUrl: 'https://api.nansen.ai'
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const config = loadConfig();
|
|
141
|
+
|
|
142
|
+
export class NansenAPI {
|
|
143
|
+
constructor(apiKey = config.apiKey, baseUrl = config.baseUrl) {
|
|
144
|
+
if (!apiKey) {
|
|
145
|
+
throw new Error('API key required. Run `nansen login` or set NANSEN_API_KEY environment variable.');
|
|
146
|
+
}
|
|
147
|
+
this.apiKey = apiKey;
|
|
148
|
+
this.baseUrl = baseUrl;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async request(endpoint, body = {}, options = {}) {
|
|
152
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
153
|
+
|
|
154
|
+
const response = await fetch(url, {
|
|
155
|
+
method: 'POST',
|
|
156
|
+
headers: {
|
|
157
|
+
'Content-Type': 'application/json',
|
|
158
|
+
'apikey': this.apiKey,
|
|
159
|
+
...options.headers
|
|
160
|
+
},
|
|
161
|
+
body: JSON.stringify(body)
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const data = await response.json();
|
|
165
|
+
|
|
166
|
+
if (!response.ok) {
|
|
167
|
+
const error = new Error(data.message || data.error || `API error: ${response.status}`);
|
|
168
|
+
error.status = response.status;
|
|
169
|
+
error.data = data;
|
|
170
|
+
throw error;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return data;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ============= Smart Money Endpoints =============
|
|
177
|
+
|
|
178
|
+
async smartMoneyNetflow(params = {}) {
|
|
179
|
+
const { chains = ['solana'], filters = {}, orderBy, pagination } = params;
|
|
180
|
+
return this.request('/api/v1/smart-money/netflow', {
|
|
181
|
+
chains,
|
|
182
|
+
filters,
|
|
183
|
+
order_by: orderBy,
|
|
184
|
+
pagination
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async smartMoneyDexTrades(params = {}) {
|
|
189
|
+
const { chains = ['solana'], filters = {}, orderBy, pagination } = params;
|
|
190
|
+
return this.request('/api/v1/smart-money/dex-trades', {
|
|
191
|
+
chains,
|
|
192
|
+
filters,
|
|
193
|
+
order_by: orderBy,
|
|
194
|
+
pagination
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async smartMoneyPerpTrades(params = {}) {
|
|
199
|
+
const { filters = {}, orderBy, pagination } = params;
|
|
200
|
+
return this.request('/api/v1/smart-money/perp-trades', {
|
|
201
|
+
filters,
|
|
202
|
+
order_by: orderBy,
|
|
203
|
+
pagination
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async smartMoneyHoldings(params = {}) {
|
|
208
|
+
const { chains = ['solana'], filters = {}, orderBy, pagination } = params;
|
|
209
|
+
return this.request('/api/v1/smart-money/holdings', {
|
|
210
|
+
chains,
|
|
211
|
+
filters,
|
|
212
|
+
order_by: orderBy,
|
|
213
|
+
pagination
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async smartMoneyDcas(params = {}) {
|
|
218
|
+
const { filters = {}, orderBy, pagination } = params;
|
|
219
|
+
return this.request('/api/v1/smart-money/dcas', {
|
|
220
|
+
filters,
|
|
221
|
+
order_by: orderBy,
|
|
222
|
+
pagination
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async smartMoneyHistoricalHoldings(params = {}) {
|
|
227
|
+
const { chains = ['solana'], filters = {}, orderBy, pagination, days = 30 } = params;
|
|
228
|
+
const to = new Date().toISOString().split('T')[0];
|
|
229
|
+
const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
|
230
|
+
return this.request('/api/v1/smart-money/historical-holdings', {
|
|
231
|
+
chains,
|
|
232
|
+
date_range: { from, to },
|
|
233
|
+
filters,
|
|
234
|
+
order_by: orderBy,
|
|
235
|
+
pagination
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ============= Profiler Endpoints =============
|
|
240
|
+
|
|
241
|
+
async addressBalance(params = {}) {
|
|
242
|
+
const { address, entityName, chain = 'ethereum', hideSpamToken = true, filters = {}, orderBy } = params;
|
|
243
|
+
if (address) {
|
|
244
|
+
const validation = validateAddress(address, chain);
|
|
245
|
+
if (!validation.valid) throw new Error(validation.error);
|
|
246
|
+
}
|
|
247
|
+
return this.request('/api/v1/profiler/address/current-balance', {
|
|
248
|
+
address,
|
|
249
|
+
entity_name: entityName,
|
|
250
|
+
chain,
|
|
251
|
+
hide_spam_token: hideSpamToken,
|
|
252
|
+
filters,
|
|
253
|
+
order_by: orderBy
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async addressLabels(params = {}) {
|
|
258
|
+
const { address, chain = 'ethereum', pagination = { page: 1, recordsPerPage: 100 } } = params;
|
|
259
|
+
if (address) {
|
|
260
|
+
const validation = validateAddress(address, chain);
|
|
261
|
+
if (!validation.valid) throw new Error(validation.error);
|
|
262
|
+
}
|
|
263
|
+
return this.request('/api/beta/profiler/address/labels', {
|
|
264
|
+
parameters: { address, chain },
|
|
265
|
+
pagination
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async addressTransactions(params = {}) {
|
|
270
|
+
const { address, chain = 'ethereum', filters = {}, orderBy, pagination } = params;
|
|
271
|
+
if (address) {
|
|
272
|
+
const validation = validateAddress(address, chain);
|
|
273
|
+
if (!validation.valid) throw new Error(validation.error);
|
|
274
|
+
}
|
|
275
|
+
return this.request('/api/v1/profiler/address/transactions', {
|
|
276
|
+
address,
|
|
277
|
+
chain,
|
|
278
|
+
filters,
|
|
279
|
+
order_by: orderBy,
|
|
280
|
+
pagination
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async addressPnl(params = {}) {
|
|
285
|
+
const { address, chain = 'ethereum' } = params;
|
|
286
|
+
if (address) {
|
|
287
|
+
const validation = validateAddress(address, chain);
|
|
288
|
+
if (!validation.valid) throw new Error(validation.error);
|
|
289
|
+
}
|
|
290
|
+
return this.request('/api/v1/profiler/address/pnl-and-trade-performance', {
|
|
291
|
+
address,
|
|
292
|
+
chain
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async entitySearch(params = {}) {
|
|
297
|
+
const { query, pagination } = params;
|
|
298
|
+
return this.request('/api/beta/profiler/entity-name-search', {
|
|
299
|
+
parameters: { query },
|
|
300
|
+
pagination
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async addressHistoricalBalances(params = {}) {
|
|
305
|
+
const { address, chain = 'ethereum', filters = {}, orderBy, pagination, days = 30 } = params;
|
|
306
|
+
if (address) {
|
|
307
|
+
const validation = validateAddress(address, chain);
|
|
308
|
+
if (!validation.valid) throw new Error(validation.error);
|
|
309
|
+
}
|
|
310
|
+
const to = new Date().toISOString().split('T')[0];
|
|
311
|
+
const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
|
312
|
+
return this.request('/api/v1/profiler/address/historical-balances', {
|
|
313
|
+
address,
|
|
314
|
+
chain,
|
|
315
|
+
date: { from, to },
|
|
316
|
+
filters,
|
|
317
|
+
order_by: orderBy,
|
|
318
|
+
pagination
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async addressRelatedWallets(params = {}) {
|
|
323
|
+
const { address, chain = 'ethereum', filters = {}, orderBy, pagination } = params;
|
|
324
|
+
if (address) {
|
|
325
|
+
const validation = validateAddress(address, chain);
|
|
326
|
+
if (!validation.valid) throw new Error(validation.error);
|
|
327
|
+
}
|
|
328
|
+
return this.request('/api/v1/profiler/address/related-wallets', {
|
|
329
|
+
address,
|
|
330
|
+
chain,
|
|
331
|
+
filters,
|
|
332
|
+
order_by: orderBy,
|
|
333
|
+
pagination
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async addressCounterparties(params = {}) {
|
|
338
|
+
const { address, chain = 'ethereum', filters = {}, orderBy, pagination, days = 30 } = params;
|
|
339
|
+
if (address) {
|
|
340
|
+
const validation = validateAddress(address, chain);
|
|
341
|
+
if (!validation.valid) throw new Error(validation.error);
|
|
342
|
+
}
|
|
343
|
+
const to = new Date().toISOString().split('T')[0];
|
|
344
|
+
const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
|
345
|
+
return this.request('/api/v1/profiler/address/counterparties', {
|
|
346
|
+
address,
|
|
347
|
+
chain,
|
|
348
|
+
date: { from, to },
|
|
349
|
+
filters,
|
|
350
|
+
order_by: orderBy,
|
|
351
|
+
pagination
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async addressPnlSummary(params = {}) {
|
|
356
|
+
const { address, chain = 'ethereum', filters = {}, orderBy, pagination, days = 30 } = params;
|
|
357
|
+
if (address) {
|
|
358
|
+
const validation = validateAddress(address, chain);
|
|
359
|
+
if (!validation.valid) throw new Error(validation.error);
|
|
360
|
+
}
|
|
361
|
+
const to = new Date().toISOString().split('T')[0];
|
|
362
|
+
const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
|
363
|
+
return this.request('/api/v1/profiler/address/pnl-summary', {
|
|
364
|
+
address,
|
|
365
|
+
chain,
|
|
366
|
+
date: { from, to },
|
|
367
|
+
filters,
|
|
368
|
+
order_by: orderBy,
|
|
369
|
+
pagination
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
async addressPerpPositions(params = {}) {
|
|
374
|
+
const { address, filters = {}, orderBy, pagination } = params;
|
|
375
|
+
// Perp positions work with HL addresses (not validated)
|
|
376
|
+
return this.request('/api/v1/profiler/perp-positions', {
|
|
377
|
+
address,
|
|
378
|
+
filters,
|
|
379
|
+
order_by: orderBy,
|
|
380
|
+
pagination
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async addressPerpTrades(params = {}) {
|
|
385
|
+
const { address, filters = {}, orderBy, pagination, days = 30 } = params;
|
|
386
|
+
const to = new Date().toISOString().split('T')[0];
|
|
387
|
+
const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
|
388
|
+
return this.request('/api/v1/profiler/perp-trades', {
|
|
389
|
+
address,
|
|
390
|
+
date: { from, to },
|
|
391
|
+
filters,
|
|
392
|
+
order_by: orderBy,
|
|
393
|
+
pagination
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ============= Token God Mode Endpoints =============
|
|
398
|
+
|
|
399
|
+
async tokenScreener(params = {}) {
|
|
400
|
+
const { chains = ['solana'], timeframe = '24h', filters = {}, orderBy, pagination } = params;
|
|
401
|
+
return this.request('/api/v1/token-screener', {
|
|
402
|
+
chains,
|
|
403
|
+
timeframe,
|
|
404
|
+
filters,
|
|
405
|
+
order_by: orderBy,
|
|
406
|
+
pagination
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async tokenHolders(params = {}) {
|
|
411
|
+
const { tokenAddress, chain = 'solana', labelType = 'all_holders', filters = {}, orderBy, pagination } = params;
|
|
412
|
+
if (tokenAddress) {
|
|
413
|
+
const validation = validateTokenAddress(tokenAddress, chain);
|
|
414
|
+
if (!validation.valid) throw new Error(validation.error);
|
|
415
|
+
}
|
|
416
|
+
return this.request('/api/v1/tgm/holders', {
|
|
417
|
+
token_address: tokenAddress,
|
|
418
|
+
chain,
|
|
419
|
+
label_type: labelType,
|
|
420
|
+
filters,
|
|
421
|
+
order_by: orderBy,
|
|
422
|
+
pagination
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async tokenFlows(params = {}) {
|
|
427
|
+
const { tokenAddress, chain = 'solana', filters = {}, orderBy, pagination } = params;
|
|
428
|
+
if (tokenAddress) {
|
|
429
|
+
const validation = validateTokenAddress(tokenAddress, chain);
|
|
430
|
+
if (!validation.valid) throw new Error(validation.error);
|
|
431
|
+
}
|
|
432
|
+
return this.request('/api/v1/tgm/flows', {
|
|
433
|
+
token_address: tokenAddress,
|
|
434
|
+
chain,
|
|
435
|
+
filters,
|
|
436
|
+
order_by: orderBy,
|
|
437
|
+
pagination
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async tokenDexTrades(params = {}) {
|
|
442
|
+
const { tokenAddress, chain = 'solana', onlySmartMoney = false, filters = {}, orderBy, pagination, days = 7 } = params;
|
|
443
|
+
if (tokenAddress) {
|
|
444
|
+
const validation = validateTokenAddress(tokenAddress, chain);
|
|
445
|
+
if (!validation.valid) throw new Error(validation.error);
|
|
446
|
+
}
|
|
447
|
+
const to = new Date().toISOString().split('T')[0];
|
|
448
|
+
const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
|
449
|
+
|
|
450
|
+
// Apply smart money filter via filters object
|
|
451
|
+
if (onlySmartMoney) {
|
|
452
|
+
filters.include_smart_money_labels = filters.include_smart_money_labels ||
|
|
453
|
+
['Fund', 'Smart Trader', '30D Smart Trader', '90D Smart Trader', '180D Smart Trader'];
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return this.request('/api/v1/tgm/dex-trades', {
|
|
457
|
+
token_address: tokenAddress,
|
|
458
|
+
chain,
|
|
459
|
+
date: { from, to },
|
|
460
|
+
filters,
|
|
461
|
+
order_by: orderBy,
|
|
462
|
+
pagination
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async tokenPnlLeaderboard(params = {}) {
|
|
467
|
+
const { tokenAddress, chain = 'solana', filters = {}, orderBy, pagination, days = 30 } = params;
|
|
468
|
+
if (tokenAddress) {
|
|
469
|
+
const validation = validateTokenAddress(tokenAddress, chain);
|
|
470
|
+
if (!validation.valid) throw new Error(validation.error);
|
|
471
|
+
}
|
|
472
|
+
const to = new Date().toISOString().split('T')[0];
|
|
473
|
+
const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
|
474
|
+
return this.request('/api/v1/tgm/pnl-leaderboard', {
|
|
475
|
+
token_address: tokenAddress,
|
|
476
|
+
chain,
|
|
477
|
+
date: { from, to },
|
|
478
|
+
filters,
|
|
479
|
+
order_by: orderBy,
|
|
480
|
+
pagination
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
async tokenWhoBoughtSold(params = {}) {
|
|
485
|
+
const { tokenAddress, chain = 'solana', filters = {}, orderBy, pagination } = params;
|
|
486
|
+
if (tokenAddress) {
|
|
487
|
+
const validation = validateTokenAddress(tokenAddress, chain);
|
|
488
|
+
if (!validation.valid) throw new Error(validation.error);
|
|
489
|
+
}
|
|
490
|
+
return this.request('/api/v1/tgm/who-bought-sold', {
|
|
491
|
+
token_address: tokenAddress,
|
|
492
|
+
chain,
|
|
493
|
+
filters,
|
|
494
|
+
order_by: orderBy,
|
|
495
|
+
pagination
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
async tokenFlowIntelligence(params = {}) {
|
|
500
|
+
const { tokenAddress, chain = 'solana', filters = {}, orderBy, pagination } = params;
|
|
501
|
+
if (tokenAddress) {
|
|
502
|
+
const validation = validateTokenAddress(tokenAddress, chain);
|
|
503
|
+
if (!validation.valid) throw new Error(validation.error);
|
|
504
|
+
}
|
|
505
|
+
return this.request('/api/v1/tgm/flow-intelligence', {
|
|
506
|
+
token_address: tokenAddress,
|
|
507
|
+
chain,
|
|
508
|
+
filters,
|
|
509
|
+
order_by: orderBy,
|
|
510
|
+
pagination
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async tokenTransfers(params = {}) {
|
|
515
|
+
const { tokenAddress, chain = 'solana', filters = {}, orderBy, pagination, days = 7 } = params;
|
|
516
|
+
if (tokenAddress) {
|
|
517
|
+
const validation = validateTokenAddress(tokenAddress, chain);
|
|
518
|
+
if (!validation.valid) throw new Error(validation.error);
|
|
519
|
+
}
|
|
520
|
+
const to = new Date().toISOString().split('T')[0];
|
|
521
|
+
const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
|
522
|
+
return this.request('/api/v1/tgm/transfers', {
|
|
523
|
+
token_address: tokenAddress,
|
|
524
|
+
chain,
|
|
525
|
+
date: { from, to },
|
|
526
|
+
filters,
|
|
527
|
+
order_by: orderBy,
|
|
528
|
+
pagination
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async tokenJupDca(params = {}) {
|
|
533
|
+
const { tokenAddress, filters = {}, orderBy, pagination } = params;
|
|
534
|
+
// JUP DCA is Solana-only
|
|
535
|
+
if (tokenAddress) {
|
|
536
|
+
const validation = validateTokenAddress(tokenAddress, 'solana');
|
|
537
|
+
if (!validation.valid) throw new Error(validation.error);
|
|
538
|
+
}
|
|
539
|
+
return this.request('/api/v1/tgm/jup-dca', {
|
|
540
|
+
token_address: tokenAddress,
|
|
541
|
+
filters,
|
|
542
|
+
order_by: orderBy,
|
|
543
|
+
pagination
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
async tokenPerpTrades(params = {}) {
|
|
548
|
+
const { tokenSymbol, filters = {}, orderBy, pagination, days = 30 } = params;
|
|
549
|
+
const to = new Date().toISOString().split('T')[0];
|
|
550
|
+
const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
|
551
|
+
return this.request('/api/v1/tgm/perp-trades', {
|
|
552
|
+
token_symbol: tokenSymbol,
|
|
553
|
+
date: { from, to },
|
|
554
|
+
filters,
|
|
555
|
+
order_by: orderBy,
|
|
556
|
+
pagination
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async tokenPerpPositions(params = {}) {
|
|
561
|
+
const { tokenSymbol, filters = {}, orderBy, pagination } = params;
|
|
562
|
+
return this.request('/api/v1/tgm/perp-positions', {
|
|
563
|
+
token_symbol: tokenSymbol,
|
|
564
|
+
filters,
|
|
565
|
+
order_by: orderBy,
|
|
566
|
+
pagination
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async tokenPerpPnlLeaderboard(params = {}) {
|
|
571
|
+
const { tokenSymbol, filters = {}, orderBy, pagination, days = 30 } = params;
|
|
572
|
+
const to = new Date().toISOString().split('T')[0];
|
|
573
|
+
const from = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
|
574
|
+
return this.request('/api/v1/tgm/perp-pnl-leaderboard', {
|
|
575
|
+
token_symbol: tokenSymbol,
|
|
576
|
+
date: { from, to },
|
|
577
|
+
filters,
|
|
578
|
+
order_by: orderBy,
|
|
579
|
+
pagination
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// ============= Portfolio Endpoints =============
|
|
584
|
+
|
|
585
|
+
async portfolioDefiHoldings(params = {}) {
|
|
586
|
+
const { walletAddress } = params;
|
|
587
|
+
return this.request('/api/v1/portfolio/defi-holdings', {
|
|
588
|
+
wallet_address: walletAddress
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
export default NansenAPI;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Nansen CLI - Command-line interface for Nansen API
|
|
4
|
+
* Designed for AI agents with structured JSON output
|
|
5
|
+
*
|
|
6
|
+
* Usage: nansen <command> [options]
|
|
7
|
+
*
|
|
8
|
+
* All output is JSON for easy parsing by AI agents.
|
|
9
|
+
* Use --pretty for human-readable formatting.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { NansenAPI, saveConfig, deleteConfig, getConfigFile } from './api.js';
|
|
13
|
+
import * as readline from 'readline';
|
|
14
|
+
|
|
15
|
+
// Parse command line arguments
|
|
16
|
+
function parseArgs(args) {
|
|
17
|
+
const result = { _: [], flags: {}, options: {} };
|
|
18
|
+
|
|
19
|
+
for (let i = 0; i < args.length; i++) {
|
|
20
|
+
const arg = args[i];
|
|
21
|
+
|
|
22
|
+
if (arg.startsWith('--')) {
|
|
23
|
+
const key = arg.slice(2);
|
|
24
|
+
const next = args[i + 1];
|
|
25
|
+
|
|
26
|
+
if (key === 'pretty' || key === 'help') {
|
|
27
|
+
result.flags[key] = true;
|
|
28
|
+
} else if (next && !next.startsWith('-')) {
|
|
29
|
+
// Try to parse as JSON first
|
|
30
|
+
try {
|
|
31
|
+
result.options[key] = JSON.parse(next);
|
|
32
|
+
} catch {
|
|
33
|
+
result.options[key] = next;
|
|
34
|
+
}
|
|
35
|
+
i++;
|
|
36
|
+
} else {
|
|
37
|
+
result.flags[key] = true;
|
|
38
|
+
}
|
|
39
|
+
} else if (arg.startsWith('-')) {
|
|
40
|
+
result.flags[arg.slice(1)] = true;
|
|
41
|
+
} else {
|
|
42
|
+
result._.push(arg);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Output helper
|
|
50
|
+
function output(data, pretty = false) {
|
|
51
|
+
if (pretty) {
|
|
52
|
+
console.log(JSON.stringify(data, null, 2));
|
|
53
|
+
} else {
|
|
54
|
+
console.log(JSON.stringify(data));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Error output
|
|
59
|
+
function errorOutput(error, pretty = false) {
|
|
60
|
+
const errorData = {
|
|
61
|
+
success: false,
|
|
62
|
+
error: error.message,
|
|
63
|
+
status: error.status,
|
|
64
|
+
details: error.data
|
|
65
|
+
};
|
|
66
|
+
output(errorData, pretty);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Help text
|
|
71
|
+
const HELP = `
|
|
72
|
+
Nansen CLI - Command-line interface for Nansen API
|
|
73
|
+
Designed for AI agents with structured JSON output.
|
|
74
|
+
|
|
75
|
+
USAGE:
|
|
76
|
+
nansen <command> [subcommand] [options]
|
|
77
|
+
|
|
78
|
+
COMMANDS:
|
|
79
|
+
login Save your API key (interactive)
|
|
80
|
+
logout Remove saved API key
|
|
81
|
+
smart-money Smart Money analytics (netflow, dex-trades, holdings, dcas, historical-holdings)
|
|
82
|
+
profiler Wallet profiling (balance, labels, transactions, pnl, perp-positions, perp-trades)
|
|
83
|
+
token Token God Mode (screener, holders, flows, trades, pnl, perp-trades, perp-positions)
|
|
84
|
+
portfolio Portfolio analytics (defi-holdings)
|
|
85
|
+
help Show this help message
|
|
86
|
+
|
|
87
|
+
GLOBAL OPTIONS:
|
|
88
|
+
--pretty Format JSON output for readability
|
|
89
|
+
--chain Blockchain to query (ethereum, solana, base, etc.)
|
|
90
|
+
--chains Multiple chains as JSON array
|
|
91
|
+
--limit Number of results (shorthand for pagination)
|
|
92
|
+
--filters JSON object with filters
|
|
93
|
+
--order-by JSON array with sort order
|
|
94
|
+
--days Date range in days (default: 30 for most endpoints)
|
|
95
|
+
--symbol Token symbol (for perp endpoints)
|
|
96
|
+
|
|
97
|
+
EXAMPLES:
|
|
98
|
+
# Get Smart Money netflow on Solana
|
|
99
|
+
nansen smart-money netflow --chain solana
|
|
100
|
+
|
|
101
|
+
# Get top tokens by Smart Money activity
|
|
102
|
+
nansen token screener --chain solana --timeframe 24h --pretty
|
|
103
|
+
|
|
104
|
+
# Get wallet balance
|
|
105
|
+
nansen profiler balance --address 0x123... --chain ethereum
|
|
106
|
+
|
|
107
|
+
# Get wallet labels
|
|
108
|
+
nansen profiler labels --address 0x123... --chain ethereum
|
|
109
|
+
|
|
110
|
+
# Search for entity
|
|
111
|
+
nansen profiler search --query "Vitalik"
|
|
112
|
+
|
|
113
|
+
# Get token holders with filters
|
|
114
|
+
nansen token holders --token 0x123... --filters '{"only_smart_money":true}'
|
|
115
|
+
|
|
116
|
+
SMART MONEY LABELS:
|
|
117
|
+
Fund, Smart Trader, 30D Smart Trader, 90D Smart Trader,
|
|
118
|
+
180D Smart Trader, Smart HL Perps Trader
|
|
119
|
+
|
|
120
|
+
SUPPORTED CHAINS:
|
|
121
|
+
ethereum, solana, base, bnb, arbitrum, polygon, optimism,
|
|
122
|
+
avalanche, linea, scroll, zksync, mantle, ronin, sei,
|
|
123
|
+
plasma, sonic, unichain, monad, hyperevm, iotaevm
|
|
124
|
+
|
|
125
|
+
For more info: https://docs.nansen.ai
|
|
126
|
+
`;
|
|
127
|
+
|
|
128
|
+
// Command handlers
|
|
129
|
+
// Helper to prompt for input
|
|
130
|
+
async function prompt(question, hidden = false) {
|
|
131
|
+
const rl = readline.createInterface({
|
|
132
|
+
input: process.stdin,
|
|
133
|
+
output: process.stdout
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return new Promise((resolve) => {
|
|
137
|
+
if (hidden && process.stdout.isTTY) {
|
|
138
|
+
process.stdout.write(question);
|
|
139
|
+
let input = '';
|
|
140
|
+
process.stdin.setRawMode(true);
|
|
141
|
+
process.stdin.resume();
|
|
142
|
+
process.stdin.setEncoding('utf8');
|
|
143
|
+
|
|
144
|
+
const onData = (char) => {
|
|
145
|
+
if (char === '\n' || char === '\r') {
|
|
146
|
+
process.stdin.setRawMode(false);
|
|
147
|
+
process.stdin.pause();
|
|
148
|
+
process.stdin.removeListener('data', onData);
|
|
149
|
+
process.stdout.write('\n');
|
|
150
|
+
rl.close();
|
|
151
|
+
resolve(input);
|
|
152
|
+
} else if (char === '\u0003') {
|
|
153
|
+
// Ctrl+C
|
|
154
|
+
process.exit();
|
|
155
|
+
} else if (char === '\u007F' || char === '\b') {
|
|
156
|
+
// Backspace
|
|
157
|
+
if (input.length > 0) {
|
|
158
|
+
input = input.slice(0, -1);
|
|
159
|
+
process.stdout.write('\b \b');
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
input += char;
|
|
163
|
+
process.stdout.write('*');
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
process.stdin.on('data', onData);
|
|
168
|
+
} else {
|
|
169
|
+
rl.question(question, (answer) => {
|
|
170
|
+
rl.close();
|
|
171
|
+
resolve(answer);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const commands = {
|
|
178
|
+
'login': async (args, api, flags) => {
|
|
179
|
+
console.log('Nansen CLI Login\n');
|
|
180
|
+
console.log('Get your API key at: https://app.nansen.ai/api\n');
|
|
181
|
+
|
|
182
|
+
const apiKey = await prompt('Enter your API key: ', true);
|
|
183
|
+
|
|
184
|
+
if (!apiKey || apiKey.trim().length === 0) {
|
|
185
|
+
console.log('\n❌ No API key provided');
|
|
186
|
+
process.exit(1);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Validate the key with a test request
|
|
190
|
+
console.log('\nValidating API key...');
|
|
191
|
+
try {
|
|
192
|
+
const testApi = new NansenAPI(apiKey.trim());
|
|
193
|
+
await testApi.tokenScreener({ chains: ['solana'], pagination: { page: 1, per_page: 1 } });
|
|
194
|
+
|
|
195
|
+
// Save the config
|
|
196
|
+
saveConfig({
|
|
197
|
+
apiKey: apiKey.trim(),
|
|
198
|
+
baseUrl: 'https://api.nansen.ai'
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
console.log('✓ API key validated');
|
|
202
|
+
console.log(`✓ Saved to ${getConfigFile()}\n`);
|
|
203
|
+
console.log('You can now use the Nansen CLI. Try:');
|
|
204
|
+
console.log(' nansen token screener --chain solana --pretty');
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.log(`\n❌ Invalid API key: ${error.message}`);
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
'logout': async (args, api, flags) => {
|
|
212
|
+
const deleted = deleteConfig();
|
|
213
|
+
if (deleted) {
|
|
214
|
+
console.log(`✓ Removed ${getConfigFile()}`);
|
|
215
|
+
} else {
|
|
216
|
+
console.log('No saved credentials found');
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
'help': async (args, api, flags) => {
|
|
221
|
+
console.log(HELP);
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
'smart-money': async (args, api, flags, options) => {
|
|
225
|
+
const subcommand = args[0] || 'help';
|
|
226
|
+
const chain = options.chain || 'solana';
|
|
227
|
+
const chains = options.chains || [chain];
|
|
228
|
+
const filters = options.filters || {};
|
|
229
|
+
const orderBy = options['order-by'];
|
|
230
|
+
const pagination = options.limit ? { page: 1, per_page: options.limit } : undefined;
|
|
231
|
+
|
|
232
|
+
// Add smart money label filter if specified
|
|
233
|
+
if (options.labels) {
|
|
234
|
+
filters.include_smart_money_labels = Array.isArray(options.labels)
|
|
235
|
+
? options.labels
|
|
236
|
+
: [options.labels];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const days = options.days ? parseInt(options.days) : 30;
|
|
240
|
+
|
|
241
|
+
const handlers = {
|
|
242
|
+
'netflow': () => api.smartMoneyNetflow({ chains, filters, orderBy, pagination }),
|
|
243
|
+
'dex-trades': () => api.smartMoneyDexTrades({ chains, filters, orderBy, pagination }),
|
|
244
|
+
'perp-trades': () => api.smartMoneyPerpTrades({ filters, orderBy, pagination }),
|
|
245
|
+
'holdings': () => api.smartMoneyHoldings({ chains, filters, orderBy, pagination }),
|
|
246
|
+
'dcas': () => api.smartMoneyDcas({ filters, orderBy, pagination }),
|
|
247
|
+
'historical-holdings': () => api.smartMoneyHistoricalHoldings({ chains, filters, orderBy, pagination, days }),
|
|
248
|
+
'help': () => ({
|
|
249
|
+
commands: ['netflow', 'dex-trades', 'perp-trades', 'holdings', 'dcas', 'historical-holdings'],
|
|
250
|
+
description: 'Smart Money analytics endpoints',
|
|
251
|
+
example: 'nansen smart-money netflow --chain solana --labels Fund'
|
|
252
|
+
})
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
if (!handlers[subcommand]) {
|
|
256
|
+
return { error: `Unknown subcommand: ${subcommand}`, available: Object.keys(handlers) };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return handlers[subcommand]();
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
'profiler': async (args, api, flags, options) => {
|
|
263
|
+
const subcommand = args[0] || 'help';
|
|
264
|
+
const address = options.address;
|
|
265
|
+
const entityName = options.entity || options['entity-name'];
|
|
266
|
+
const chain = options.chain || 'ethereum';
|
|
267
|
+
const filters = options.filters || {};
|
|
268
|
+
const orderBy = options['order-by'];
|
|
269
|
+
const pagination = options.limit ? { page: 1, recordsPerPage: options.limit } : undefined;
|
|
270
|
+
const days = options.days ? parseInt(options.days) : 30;
|
|
271
|
+
|
|
272
|
+
const handlers = {
|
|
273
|
+
'balance': () => api.addressBalance({ address, entityName, chain, filters, orderBy }),
|
|
274
|
+
'labels': () => api.addressLabels({ address, chain, pagination }),
|
|
275
|
+
'transactions': () => api.addressTransactions({ address, chain, filters, orderBy, pagination }),
|
|
276
|
+
'pnl': () => api.addressPnl({ address, chain }),
|
|
277
|
+
'search': () => api.entitySearch({ query: options.query, pagination }),
|
|
278
|
+
'historical-balances': () => api.addressHistoricalBalances({ address, chain, filters, orderBy, pagination, days }),
|
|
279
|
+
'related-wallets': () => api.addressRelatedWallets({ address, chain, filters, orderBy, pagination }),
|
|
280
|
+
'counterparties': () => api.addressCounterparties({ address, chain, filters, orderBy, pagination, days }),
|
|
281
|
+
'pnl-summary': () => api.addressPnlSummary({ address, chain, filters, orderBy, pagination, days }),
|
|
282
|
+
'perp-positions': () => api.addressPerpPositions({ address, filters, orderBy, pagination }),
|
|
283
|
+
'perp-trades': () => api.addressPerpTrades({ address, filters, orderBy, pagination, days }),
|
|
284
|
+
'help': () => ({
|
|
285
|
+
commands: ['balance', 'labels', 'transactions', 'pnl', 'search', 'historical-balances', 'related-wallets', 'counterparties', 'pnl-summary', 'perp-positions', 'perp-trades'],
|
|
286
|
+
description: 'Wallet profiling endpoints',
|
|
287
|
+
example: 'nansen profiler balance --address 0x123... --chain ethereum'
|
|
288
|
+
})
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
if (!handlers[subcommand]) {
|
|
292
|
+
return { error: `Unknown subcommand: ${subcommand}`, available: Object.keys(handlers) };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return handlers[subcommand]();
|
|
296
|
+
},
|
|
297
|
+
|
|
298
|
+
'token': async (args, api, flags, options) => {
|
|
299
|
+
const subcommand = args[0] || 'help';
|
|
300
|
+
const tokenAddress = options.token || options['token-address'];
|
|
301
|
+
const tokenSymbol = options.symbol || options['token-symbol'];
|
|
302
|
+
const chain = options.chain || 'solana';
|
|
303
|
+
const chains = options.chains || [chain];
|
|
304
|
+
const timeframe = options.timeframe || '24h';
|
|
305
|
+
const filters = options.filters || {};
|
|
306
|
+
const orderBy = options['order-by'];
|
|
307
|
+
const pagination = options.limit ? { page: 1, per_page: options.limit } : undefined;
|
|
308
|
+
const days = options.days ? parseInt(options.days) : 30;
|
|
309
|
+
|
|
310
|
+
// Convenience filter for smart money only
|
|
311
|
+
const onlySmartMoney = options['smart-money'] || flags['smart-money'] || false;
|
|
312
|
+
if (onlySmartMoney) {
|
|
313
|
+
filters.only_smart_money = true;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const handlers = {
|
|
317
|
+
'screener': () => api.tokenScreener({ chains, timeframe, filters, orderBy, pagination }),
|
|
318
|
+
'holders': () => api.tokenHolders({ tokenAddress, chain, filters, orderBy, pagination }),
|
|
319
|
+
'flows': () => api.tokenFlows({ tokenAddress, chain, filters, orderBy, pagination }),
|
|
320
|
+
'dex-trades': () => api.tokenDexTrades({ tokenAddress, chain, onlySmartMoney, filters, orderBy, pagination, days }),
|
|
321
|
+
'pnl': () => api.tokenPnlLeaderboard({ tokenAddress, chain, filters, orderBy, pagination, days }),
|
|
322
|
+
'who-bought-sold': () => api.tokenWhoBoughtSold({ tokenAddress, chain, filters, orderBy, pagination }),
|
|
323
|
+
'flow-intelligence': () => api.tokenFlowIntelligence({ tokenAddress, chain, filters, orderBy, pagination }),
|
|
324
|
+
'transfers': () => api.tokenTransfers({ tokenAddress, chain, filters, orderBy, pagination, days }),
|
|
325
|
+
'jup-dca': () => api.tokenJupDca({ tokenAddress, filters, orderBy, pagination }),
|
|
326
|
+
'perp-trades': () => api.tokenPerpTrades({ tokenSymbol, filters, orderBy, pagination, days }),
|
|
327
|
+
'perp-positions': () => api.tokenPerpPositions({ tokenSymbol, filters, orderBy, pagination }),
|
|
328
|
+
'perp-pnl-leaderboard': () => api.tokenPerpPnlLeaderboard({ tokenSymbol, filters, orderBy, pagination, days }),
|
|
329
|
+
'help': () => ({
|
|
330
|
+
commands: ['screener', 'holders', 'flows', 'dex-trades', 'pnl', 'who-bought-sold', 'flow-intelligence', 'transfers', 'jup-dca', 'perp-trades', 'perp-positions', 'perp-pnl-leaderboard'],
|
|
331
|
+
description: 'Token God Mode endpoints',
|
|
332
|
+
example: 'nansen token screener --chain solana --timeframe 24h --smart-money'
|
|
333
|
+
})
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
if (!handlers[subcommand]) {
|
|
337
|
+
return { error: `Unknown subcommand: ${subcommand}`, available: Object.keys(handlers) };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return handlers[subcommand]();
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
'portfolio': async (args, api, flags, options) => {
|
|
344
|
+
const subcommand = args[0] || 'help';
|
|
345
|
+
const walletAddress = options.wallet || options.address;
|
|
346
|
+
|
|
347
|
+
const handlers = {
|
|
348
|
+
'defi': () => api.portfolioDefiHoldings({ walletAddress }),
|
|
349
|
+
'defi-holdings': () => api.portfolioDefiHoldings({ walletAddress }),
|
|
350
|
+
'help': () => ({
|
|
351
|
+
commands: ['defi', 'defi-holdings'],
|
|
352
|
+
description: 'Portfolio analytics endpoints',
|
|
353
|
+
example: 'nansen portfolio defi --wallet 0x123...'
|
|
354
|
+
})
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
if (!handlers[subcommand]) {
|
|
358
|
+
return { error: `Unknown subcommand: ${subcommand}`, available: Object.keys(handlers) };
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return handlers[subcommand]();
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
// Main entry point
|
|
366
|
+
async function main() {
|
|
367
|
+
const rawArgs = process.argv.slice(2);
|
|
368
|
+
const { _: positional, flags, options } = parseArgs(rawArgs);
|
|
369
|
+
|
|
370
|
+
const command = positional[0] || 'help';
|
|
371
|
+
const subArgs = positional.slice(1);
|
|
372
|
+
const pretty = flags.pretty || flags.p;
|
|
373
|
+
|
|
374
|
+
if (command === 'help' || flags.help || flags.h) {
|
|
375
|
+
console.log(HELP);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (!commands[command]) {
|
|
380
|
+
output({
|
|
381
|
+
error: `Unknown command: ${command}`,
|
|
382
|
+
available: Object.keys(commands)
|
|
383
|
+
}, pretty);
|
|
384
|
+
process.exit(1);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Commands that don't require API authentication
|
|
388
|
+
const noAuthCommands = ['login', 'logout', 'help'];
|
|
389
|
+
|
|
390
|
+
if (noAuthCommands.includes(command)) {
|
|
391
|
+
await commands[command](subArgs, null, flags, options);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
const api = new NansenAPI();
|
|
397
|
+
const result = await commands[command](subArgs, api, flags, options);
|
|
398
|
+
output({ success: true, data: result }, pretty);
|
|
399
|
+
} catch (error) {
|
|
400
|
+
errorOutput(error, pretty);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
main();
|