nansen-cli 1.20.0 → 1.21.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/CHANGELOG.md +16 -0
- package/README.md +26 -1
- package/package.json +1 -1
- package/skills/nansen-trading/SKILL.md +5 -1
- package/skills/nansen-wallet-manager/SKILL.md +12 -0
- package/src/api.js +4 -4
- package/src/cli.js +27 -19
- package/src/commands/agent.js +344 -0
- package/src/cost-cache.js +67 -0
- package/src/schema.json +76 -0
- package/src/wallet.js +7 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.21.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#315](https://github.com/nansen-ai/nansen-cli/pull/315) [`908fa0c`](https://github.com/nansen-ai/nansen-cli/commit/908fa0ccdf18cb79b7efb58b9b31f66e0afedff6) Thanks [@TimNooren](https://github.com/TimNooren)! - Add `nansen agent` command for the Nansen AI research agent with fast/expert modes, SSE streaming, conversation continuation, and JSON output.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- [#326](https://github.com/nansen-ai/nansen-cli/pull/326) [`1532ba4`](https://github.com/nansen-ai/nansen-cli/commit/1532ba420174ed7635a42641f0c1a2802077fdc0) Thanks [@TimNooren](https://github.com/TimNooren)! - Show API credit cost in research subcommand help text (fetched from OpenAPI spec, cached 24h).
|
|
12
|
+
|
|
13
|
+
- [#330](https://github.com/nansen-ai/nansen-cli/pull/330) [`a6b9b8f`](https://github.com/nansen-ai/nansen-cli/commit/a6b9b8fc7d291ee7375941bb275c224939338161) Thanks [@0xlaveen](https://github.com/0xlaveen)! - Suppress misleading PASSWORD_REQUIRED error when `--provider privy` is specified. Privy wallets don't need a password — only the Privy-specific credentials error is now shown when PRIVY_APP_ID/PRIVY_APP_SECRET are missing.
|
|
14
|
+
|
|
15
|
+
- [#329](https://github.com/nansen-ai/nansen-cli/pull/329) [`f047833`](https://github.com/nansen-ai/nansen-cli/commit/f047833c7e6fb55cb713e0b69f82282fe87d4566) Thanks [@TimNooren](https://github.com/TimNooren)! - Limit deprecation warnings and update notices to help output only, keeping stdout/stderr clean for programmatic usage.
|
|
16
|
+
|
|
17
|
+
- [#332](https://github.com/nansen-ai/nansen-cli/pull/332) [`e9b6de1`](https://github.com/nansen-ai/nansen-cli/commit/e9b6de17fdba3f46733dff026c2495c095bfbf35) Thanks [@0xlaveen](https://github.com/0xlaveen)! - docs: add trading examples and Privy wallet setup to README
|
|
18
|
+
|
|
3
19
|
## 1.20.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -36,10 +36,35 @@ nansen schema [command] [--pretty] # full command reference (no API key neede
|
|
|
36
36
|
|
|
37
37
|
**Trade:** `quote`, `execute` — DEX swaps on Solana and Base.
|
|
38
38
|
|
|
39
|
-
**Wallet:** `create`, `list`, `show`, `export`, `default`, `delete`, `send` — local
|
|
39
|
+
**Wallet:** `create`, `list`, `show`, `export`, `default`, `delete`, `send` — local or Privy server-side wallets (EVM + Solana).
|
|
40
40
|
|
|
41
41
|
Run `nansen schema --pretty` for the full subcommand and field reference.
|
|
42
42
|
|
|
43
|
+
## Trading
|
|
44
|
+
|
|
45
|
+
DEX swaps on `solana` and `base`. Two-step: quote then execute.
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
nansen trade quote --chain solana --from SOL --to USDC --amount 1000000000
|
|
49
|
+
nansen trade execute --quote <quoteId>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Amounts are in base units (lamports, wei). Common symbols (`SOL`, `ETH`, `USDC`, `USDT`) resolve automatically. A wallet is required — set one with `nansen wallet default <name>`.
|
|
53
|
+
|
|
54
|
+
## Wallet
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
nansen wallet create --name my-wallet # local keypair (EVM + Solana)
|
|
58
|
+
nansen wallet create --name my-wallet --provider privy # server-side via Privy
|
|
59
|
+
nansen wallet list
|
|
60
|
+
nansen wallet default <name>
|
|
61
|
+
nansen wallet send --wallet <name> --to <addr> --amount <n> --chain <chain>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Local wallets** are password-encrypted. Set `NANSEN_WALLET_PASSWORD` to skip the prompt.
|
|
65
|
+
|
|
66
|
+
**Privy wallets** are server-side — no password, no local key storage. Requires `PRIVY_APP_ID` and `PRIVY_APP_SECRET` env vars. Get credentials at [dashboard.privy.io](https://dashboard.privy.io).
|
|
67
|
+
|
|
43
68
|
## Key Options
|
|
44
69
|
|
|
45
70
|
| Option | Description |
|
package/package.json
CHANGED
|
@@ -58,7 +58,9 @@ nansen trade execute --quote "$quote_id"
|
|
|
58
58
|
| ETH | Base | `0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee` |
|
|
59
59
|
| USDC | Base | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` |
|
|
60
60
|
|
|
61
|
-
## Amounts are in base units
|
|
61
|
+
## Amounts are in base units — NEVER USD
|
|
62
|
+
|
|
63
|
+
`--amount` accepts **integer base units only** (lamports, wei, etc). It is never a USD value.
|
|
62
64
|
|
|
63
65
|
| Token | Decimals | 1 token = |
|
|
64
66
|
|-------|----------|-----------|
|
|
@@ -66,6 +68,8 @@ nansen trade execute --quote "$quote_id"
|
|
|
66
68
|
| ETH | 18 | `1000000000000000000` |
|
|
67
69
|
| USDC | 6 | `1000000` |
|
|
68
70
|
|
|
71
|
+
If the user says "$20 worth of X", you must convert USD → token amount → base units. For example, to buy $20 of SOL at $150/SOL: $20 ÷ $150 = 0.1333 SOL = 133,300,000 lamports → `--amount 133300000`. Use a price lookup (e.g. `nansen research token info`) to get the current price first.
|
|
72
|
+
|
|
69
73
|
## Flags
|
|
70
74
|
|
|
71
75
|
| Flag | Purpose |
|
|
@@ -109,6 +109,18 @@ If the `.credentials` file fallback is used, the CLI prints a warning on every o
|
|
|
109
109
|
|
|
110
110
|
## Create
|
|
111
111
|
|
|
112
|
+
### Privy (server-side, no password)
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
nansen wallet create --provider privy
|
|
116
|
+
# Or with a custom name:
|
|
117
|
+
nansen wallet create --name trading --provider privy
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Requires `PRIVY_APP_ID` + `PRIVY_APP_SECRET` env vars. No password needed.
|
|
121
|
+
|
|
122
|
+
### Local (encrypted on disk, password required)
|
|
123
|
+
|
|
112
124
|
```bash
|
|
113
125
|
# Ask the user for a password first, then:
|
|
114
126
|
NANSEN_WALLET_PASSWORD="<password_from_user>" nansen wallet create
|
package/src/api.js
CHANGED
|
@@ -11,12 +11,12 @@ import { getAnonymousId, TELEMETRY_DISABLED } from './telemetry.js';
|
|
|
11
11
|
|
|
12
12
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
13
|
|
|
14
|
-
function telemetryHeaders() {
|
|
14
|
+
export function telemetryHeaders() {
|
|
15
15
|
if (TELEMETRY_DISABLED) return {};
|
|
16
16
|
return { 'X-Anonymous-Id': getAnonymousId() };
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
const { version: packageVersion } = JSON.parse(
|
|
19
|
+
export const { version: packageVersion } = JSON.parse(
|
|
20
20
|
fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')
|
|
21
21
|
);
|
|
22
22
|
|
|
@@ -85,7 +85,7 @@ export class NansenError extends Error {
|
|
|
85
85
|
/**
|
|
86
86
|
* Map HTTP status codes to error codes
|
|
87
87
|
*/
|
|
88
|
-
function statusToErrorCode(status, data = {}) {
|
|
88
|
+
export function statusToErrorCode(status, data = {}) {
|
|
89
89
|
const message = data?.message || data?.error || '';
|
|
90
90
|
const messageLower = message.toLowerCase();
|
|
91
91
|
|
|
@@ -486,7 +486,7 @@ export class NansenAPI {
|
|
|
486
486
|
return cached;
|
|
487
487
|
}
|
|
488
488
|
}
|
|
489
|
-
|
|
489
|
+
|
|
490
490
|
let lastError;
|
|
491
491
|
|
|
492
492
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
package/src/cli.js
CHANGED
|
@@ -7,9 +7,11 @@ import { NansenAPI, NansenError, ErrorCode, saveConfig, deleteConfig, getConfigF
|
|
|
7
7
|
import { buildWalletCommands } from './wallet.js';
|
|
8
8
|
import { buildTradingCommands } from './trading.js';
|
|
9
9
|
import { formatAlertsTable, buildAlertsCommands } from './commands/alerts.js';
|
|
10
|
+
import { buildAgentCommands } from './commands/agent.js';
|
|
10
11
|
import { resolveAddress, isEnsName } from './ens.js';
|
|
11
12
|
import fs from 'fs';
|
|
12
13
|
import { getUpdateNotification, getUpgradeNotice, scheduleUpdateCheck } from './update-check.js';
|
|
14
|
+
import { refreshCostMapIfStale, getCostForEndpoint } from './cost-cache.js';
|
|
13
15
|
import { trackCommandSucceeded, trackCommandFailed } from './telemetry.js';
|
|
14
16
|
import { createRequire } from 'module';
|
|
15
17
|
import * as readline from 'readline';
|
|
@@ -165,7 +167,7 @@ export function parseArgs(args) {
|
|
|
165
167
|
const key = arg.slice(2);
|
|
166
168
|
const next = args[i + 1];
|
|
167
169
|
|
|
168
|
-
if (key === 'pretty' || key === 'help' || key === 'version' || key === 'table' || key === 'no-retry' || key === 'cache' || key === 'no-cache' || key === 'stream' || key === 'enrich' || key === 'full' || key === 'human' || key === 'enabled' || key === 'disabled') {
|
|
170
|
+
if (key === 'pretty' || key === 'help' || key === 'version' || key === 'table' || key === 'no-retry' || key === 'cache' || key === 'no-cache' || key === 'stream' || key === 'enrich' || key === 'full' || key === 'human' || key === 'enabled' || key === 'disabled' || key === 'expert' || key === 'json') {
|
|
169
171
|
result.flags[key] = true;
|
|
170
172
|
} else if (next && (!next.startsWith('-') || /^-\d/.test(next))) {
|
|
171
173
|
// Try to parse as JSON first (for objects/arrays/booleans),
|
|
@@ -682,6 +684,7 @@ COMMANDS:
|
|
|
682
684
|
research smart-money, profiler, token, search, perp, portfolio, points
|
|
683
685
|
trade quote, execute
|
|
684
686
|
wallet create, list, show, export, default, delete, forget-password
|
|
687
|
+
agent Ask the Nansen AI research agent (fast/expert modes)
|
|
685
688
|
alerts list, create, update, toggle, delete
|
|
686
689
|
web search, fetch
|
|
687
690
|
account Show API key status, plan, and remaining credits
|
|
@@ -701,6 +704,10 @@ EXAMPLES:
|
|
|
701
704
|
nansen research profiler balance --address 0x... --chain ethereum
|
|
702
705
|
nansen trade quote --chain base --from ETH --to USDC --amount 1000000000000000000
|
|
703
706
|
|
|
707
|
+
DEPRECATED ALIASES (still work, will be removed in a future version):
|
|
708
|
+
smart-money, profiler, token, search, perp, portfolio, points → use "nansen research <command>"
|
|
709
|
+
quote, execute → use "nansen trade <command>"
|
|
710
|
+
|
|
704
711
|
Research chains: ethereum, solana, base, bnb, arbitrum, polygon, optimism, avalanche, linea, scroll, mantle, ronin, sei, plasma, sonic, monad, hyperevm, iotaevm
|
|
705
712
|
Trade chains: solana, base
|
|
706
713
|
Labels: Fund, Smart Trader, 30D/90D/180D Smart Trader, Smart HL Perps Trader
|
|
@@ -1509,6 +1516,11 @@ export function generateSubcommandHelp(command, subcommand, prefix = null) {
|
|
|
1509
1516
|
lines.push(`Params (* required): ${params.join(', ')}`);
|
|
1510
1517
|
}
|
|
1511
1518
|
|
|
1519
|
+
if (subSchema.endpoint) {
|
|
1520
|
+
const cost = getCostForEndpoint(subSchema.endpoint);
|
|
1521
|
+
if (cost) lines.push(`Cost: ${cost.free} credit${cost.free === 1 ? '' : 's'} (Free tier) / ${cost.pro} credit${cost.pro === 1 ? '' : 's'} (Pro tier)`);
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1512
1524
|
if (subSchema.returns?.length) {
|
|
1513
1525
|
lines.push(`Returns: ${subSchema.returns.join(', ')}`);
|
|
1514
1526
|
}
|
|
@@ -1548,12 +1560,7 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1548
1560
|
const subArgs = positional.slice(1);
|
|
1549
1561
|
const subcommand = subArgs[0];
|
|
1550
1562
|
|
|
1551
|
-
|
|
1552
|
-
if (DEPRECATED_TO_RESEARCH.has(command)) {
|
|
1553
|
-
errorOutput(`Warning: "nansen ${command}" is deprecated. Use "nansen research ${command}" instead.`);
|
|
1554
|
-
} else if (DEPRECATED_TO_TRADE.has(command)) {
|
|
1555
|
-
errorOutput(`Warning: "nansen ${command}" is deprecated. Use "nansen trade ${command}" instead.`);
|
|
1556
|
-
}
|
|
1563
|
+
|
|
1557
1564
|
|
|
1558
1565
|
const pretty = flags.pretty || flags.p;
|
|
1559
1566
|
const table = flags.table || flags.t;
|
|
@@ -1569,7 +1576,14 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1569
1576
|
if (updateNotification) errorOutput(updateNotification);
|
|
1570
1577
|
};
|
|
1571
1578
|
|
|
1572
|
-
|
|
1579
|
+
// Deprecation note for help output
|
|
1580
|
+
const deprecationNote = (cmd) => {
|
|
1581
|
+
if (DEPRECATED_TO_RESEARCH.has(cmd)) return `Note: "nansen ${cmd}" is deprecated. Use "nansen research ${cmd}" instead.\n\n`;
|
|
1582
|
+
if (DEPRECATED_TO_TRADE.has(cmd)) return `Note: "nansen ${cmd}" is deprecated. Use "nansen trade ${cmd}" instead.\n\n`;
|
|
1583
|
+
return '';
|
|
1584
|
+
};
|
|
1585
|
+
|
|
1586
|
+
const commands = { ...buildCommands(deps), ...buildWalletCommands(deps), ...buildTradingCommands(deps), ...buildAlertsCommands(deps), ...buildAgentCommands(deps), ...commandOverrides };
|
|
1573
1587
|
|
|
1574
1588
|
if (flags.version || flags.v) {
|
|
1575
1589
|
output(VERSION);
|
|
@@ -1577,6 +1591,7 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1577
1591
|
}
|
|
1578
1592
|
|
|
1579
1593
|
if (command === 'help' || flags.help || flags.h) {
|
|
1594
|
+
await refreshCostMapIfStale();
|
|
1580
1595
|
// Check for subcommand-specific help: nansen <command> <subcommand> --help
|
|
1581
1596
|
if (flags.help || flags.h) {
|
|
1582
1597
|
// Handle 'research <category> <sub> --help' (3-level)
|
|
@@ -1607,17 +1622,17 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1607
1622
|
}
|
|
1608
1623
|
// First try subcommand help
|
|
1609
1624
|
// Skip for 'trade'/'alerts' — their handlers show their own rich usage
|
|
1610
|
-
if (command && subcommand && command !== 'trade' && command !== 'alerts') {
|
|
1625
|
+
if (command && subcommand && command !== 'trade' && command !== 'alerts' && command !== 'agent') {
|
|
1611
1626
|
const subHelp = generateSubcommandHelp(command, subcommand);
|
|
1612
1627
|
if (subHelp) {
|
|
1613
|
-
output(subHelp);
|
|
1628
|
+
output(deprecationNote(command) + subHelp);
|
|
1614
1629
|
notify();
|
|
1615
1630
|
return { type: 'subcommand-help', command, subcommand };
|
|
1616
1631
|
}
|
|
1617
1632
|
}
|
|
1618
1633
|
// Then try command-level help (list subcommands)
|
|
1619
1634
|
// Skip for 'trade'/'alerts' — let the handler show its own usage
|
|
1620
|
-
const cmdSchemaLookup = command !== 'trade' && command !== 'alerts' && (SCHEMA.commands[command] || SCHEMA.commands.research.subcommands[command]);
|
|
1635
|
+
const cmdSchemaLookup = command !== 'trade' && command !== 'alerts' && command !== 'agent' && (SCHEMA.commands[command] || SCHEMA.commands.research.subcommands[command]);
|
|
1621
1636
|
if (command && cmdSchemaLookup) {
|
|
1622
1637
|
const cmdSchema = cmdSchemaLookup;
|
|
1623
1638
|
const lines = [`${command} — ${cmdSchema.description}`];
|
|
@@ -1638,7 +1653,7 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1638
1653
|
if (cmdSchema.examples?.length) {
|
|
1639
1654
|
lines.push(`\nExamples:\n ${cmdSchema.examples.join('\n ')}`);
|
|
1640
1655
|
}
|
|
1641
|
-
output(lines.join('\n'));
|
|
1656
|
+
output(deprecationNote(command) + lines.join('\n'));
|
|
1642
1657
|
notify();
|
|
1643
1658
|
return { type: 'command-help', command };
|
|
1644
1659
|
}
|
|
@@ -1681,7 +1696,6 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1681
1696
|
const formatted = formatOutput(errorData, { pretty, table });
|
|
1682
1697
|
output(formatted.text);
|
|
1683
1698
|
trackCommandFailed({ command: fullCommand, duration_ms: Date.now() - startTime, error_code: 'UNKNOWN_COMMAND', flags: usedFlags, chain });
|
|
1684
|
-
notify();
|
|
1685
1699
|
exit(1);
|
|
1686
1700
|
return { type: 'error', data: errorData };
|
|
1687
1701
|
}
|
|
@@ -1709,7 +1723,6 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1709
1723
|
// Commands that handle their own output return undefined
|
|
1710
1724
|
if (result === undefined) {
|
|
1711
1725
|
trackCommandSucceeded({ command: fullCommand, duration_ms: Date.now() - startTime, flags: usedFlags, chain });
|
|
1712
|
-
notify();
|
|
1713
1726
|
return { type: 'no-output', command };
|
|
1714
1727
|
}
|
|
1715
1728
|
|
|
@@ -1718,7 +1731,6 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1718
1731
|
const formatted = formatOutput(result, { pretty, table: false });
|
|
1719
1732
|
output(formatted.text);
|
|
1720
1733
|
trackCommandSucceeded({ command: fullCommand, duration_ms: Date.now() - startTime, flags: usedFlags, chain });
|
|
1721
|
-
notify();
|
|
1722
1734
|
return { type: 'schema', data: result };
|
|
1723
1735
|
}
|
|
1724
1736
|
|
|
@@ -1731,7 +1743,6 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1731
1743
|
// Alerts list with --table uses custom table format
|
|
1732
1744
|
if (command === 'alerts' && subcommand === 'list' && table) {
|
|
1733
1745
|
output(formatAlertsTable(result));
|
|
1734
|
-
notify();
|
|
1735
1746
|
return { type: 'success', data: result };
|
|
1736
1747
|
}
|
|
1737
1748
|
|
|
@@ -1743,7 +1754,6 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1743
1754
|
output(streamOutput);
|
|
1744
1755
|
}
|
|
1745
1756
|
trackCommandSucceeded({ command: fullCommand, duration_ms: Date.now() - startTime, from_cache: !!result?.fromCache, flags: usedFlags, chain });
|
|
1746
|
-
notify();
|
|
1747
1757
|
return { type: 'stream', data: result };
|
|
1748
1758
|
}
|
|
1749
1759
|
|
|
@@ -1751,7 +1761,6 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1751
1761
|
const formatted = formatOutput(successData, { pretty, table, csv });
|
|
1752
1762
|
output(formatted.text);
|
|
1753
1763
|
trackCommandSucceeded({ command: fullCommand, duration_ms: Date.now() - startTime, from_cache: !!result?.fromCache, flags: usedFlags, chain });
|
|
1754
|
-
notify();
|
|
1755
1764
|
return { type: csv ? 'csv' : 'success', data: result };
|
|
1756
1765
|
} catch (error) {
|
|
1757
1766
|
const errorData = formatError(error);
|
|
@@ -1765,7 +1774,6 @@ export async function runCLI(rawArgs, deps = {}) {
|
|
|
1765
1774
|
flags: usedFlags,
|
|
1766
1775
|
chain,
|
|
1767
1776
|
});
|
|
1768
|
-
notify();
|
|
1769
1777
|
exit(1);
|
|
1770
1778
|
return { type: 'error', data: errorData };
|
|
1771
1779
|
}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nansen CLI - Agent command
|
|
3
|
+
* Interactive research agent with fast/expert modes via SSE streaming.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import crypto from 'crypto';
|
|
7
|
+
import { NansenError, ErrorCode, statusToErrorCode, telemetryHeaders, packageVersion } from '../api.js';
|
|
8
|
+
import { getCostForEndpoint } from '../cost-cache.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Build standard request headers, matching apiInstance.request() conventions.
|
|
12
|
+
*/
|
|
13
|
+
function buildHeaders(apiInstance) {
|
|
14
|
+
return {
|
|
15
|
+
'Content-Type': 'application/json',
|
|
16
|
+
'X-Client-Type': 'nansen-cli',
|
|
17
|
+
'X-Client-Version': packageVersion,
|
|
18
|
+
...telemetryHeaders(),
|
|
19
|
+
...(apiInstance.apiKey ? { 'apikey': apiInstance.apiKey } : {}),
|
|
20
|
+
...(apiInstance.defaultHeaders || {}),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Throw a NansenError with the same structure as apiInstance.request() errors.
|
|
26
|
+
* Includes `details` field for consistency with other commands.
|
|
27
|
+
*/
|
|
28
|
+
function throwApiError(message, status, serverDetail) {
|
|
29
|
+
// Match the friendly wrapper messages from apiInstance.request()
|
|
30
|
+
let friendlyMessage = message;
|
|
31
|
+
if (status === 401) {
|
|
32
|
+
friendlyMessage = 'Not logged in. Run: nansen login';
|
|
33
|
+
} else if (status === 429) {
|
|
34
|
+
friendlyMessage = 'Rate limited. Try again in a few seconds.';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
throw new NansenError(
|
|
38
|
+
friendlyMessage,
|
|
39
|
+
statusToErrorCode(status),
|
|
40
|
+
status,
|
|
41
|
+
{ detail: serverDetail || message, attempt: 1, retryAfterMs: null },
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Process an SSE response from the agent endpoint.
|
|
47
|
+
*
|
|
48
|
+
* In buffered mode (no callbacks), collects everything and returns it.
|
|
49
|
+
* In streaming mode (callbacks provided), invokes them as events arrive.
|
|
50
|
+
*
|
|
51
|
+
* @param {Response} response – fetch Response with SSE body
|
|
52
|
+
* @param {object} [callbacks]
|
|
53
|
+
* @param {Function} [callbacks.onDelta] – called with each text chunk
|
|
54
|
+
* @param {Function} [callbacks.onToolCall] – called with each tool name
|
|
55
|
+
* @returns {{ text: string, toolCalls: string[], conversationId: string|null }}
|
|
56
|
+
*/
|
|
57
|
+
export async function consumeSSEStream(response, callbacks = {}) {
|
|
58
|
+
const { onDelta, onToolCall } = callbacks;
|
|
59
|
+
const chunks = [];
|
|
60
|
+
const toolCalls = [];
|
|
61
|
+
let conversationId = null;
|
|
62
|
+
let errorPayload = null;
|
|
63
|
+
|
|
64
|
+
const reader = response.body;
|
|
65
|
+
const decoder = new TextDecoder();
|
|
66
|
+
let buffer = '';
|
|
67
|
+
|
|
68
|
+
for await (const raw of reader) {
|
|
69
|
+
buffer += decoder.decode(raw, { stream: true });
|
|
70
|
+
|
|
71
|
+
// Normalize \r\n and \r to \n (SSE spec allows all three line terminators)
|
|
72
|
+
buffer = buffer.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
73
|
+
|
|
74
|
+
// SSE: split on double-newline boundaries
|
|
75
|
+
let boundary;
|
|
76
|
+
let done = false;
|
|
77
|
+
while ((boundary = buffer.indexOf('\n\n')) !== -1) {
|
|
78
|
+
const frame = buffer.slice(0, boundary);
|
|
79
|
+
buffer = buffer.slice(boundary + 2);
|
|
80
|
+
|
|
81
|
+
for (const line of frame.split('\n')) {
|
|
82
|
+
if (!line.startsWith('data: ')) continue;
|
|
83
|
+
const payload = line.slice(6);
|
|
84
|
+
if (payload === '[DONE]') { done = true; break; }
|
|
85
|
+
|
|
86
|
+
let event;
|
|
87
|
+
try {
|
|
88
|
+
event = JSON.parse(payload);
|
|
89
|
+
} catch {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
switch (event.type) {
|
|
94
|
+
case 'delta':
|
|
95
|
+
if (event.text) {
|
|
96
|
+
chunks.push(event.text);
|
|
97
|
+
if (onDelta) onDelta(event.text);
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
case 'tool_call':
|
|
101
|
+
if (event.name) {
|
|
102
|
+
toolCalls.push(event.name);
|
|
103
|
+
if (onToolCall) onToolCall(event.name);
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
case 'finish':
|
|
107
|
+
conversationId = event.conversation_id ?? null;
|
|
108
|
+
break;
|
|
109
|
+
case 'error':
|
|
110
|
+
errorPayload = event;
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (done) break;
|
|
115
|
+
}
|
|
116
|
+
if (done) break;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (errorPayload) {
|
|
120
|
+
const status = errorPayload.status_code || 502;
|
|
121
|
+
throwApiError(
|
|
122
|
+
errorPayload.error || 'Agent request failed',
|
|
123
|
+
status,
|
|
124
|
+
errorPayload.error,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { text: chunks.join(''), toolCalls, conversationId };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Build the `agent` command handler.
|
|
133
|
+
*
|
|
134
|
+
* @param {object} [deps]
|
|
135
|
+
* @param {Function} [deps.log] – stdout line output (default: console.log)
|
|
136
|
+
* @param {Function} [deps.errorLog] – stderr line output (default: console.error)
|
|
137
|
+
* @param {Function} [deps.write] – raw stdout writer, no trailing newline (default: process.stdout.write)
|
|
138
|
+
* @returns {object} command map
|
|
139
|
+
*/
|
|
140
|
+
export function buildAgentCommands(deps = {}) {
|
|
141
|
+
const {
|
|
142
|
+
log = console.log,
|
|
143
|
+
errorLog = console.error,
|
|
144
|
+
write = (s) => process.stdout.write(s),
|
|
145
|
+
} = deps;
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
'agent': async (args, apiInstance, flags, options) => {
|
|
149
|
+
// ── Help ──
|
|
150
|
+
if (flags.help || flags.h || args[0] === 'help' || args.length === 0) {
|
|
151
|
+
const fmtCost = (c) => `${c.free} credit${c.free === 1 ? '' : 's'} (Free tier) / ${c.pro} credit${c.pro === 1 ? '' : 's'} (Pro tier)`;
|
|
152
|
+
const fastCost = getCostForEndpoint('/api/v1/agent/fast');
|
|
153
|
+
const expertCost = getCostForEndpoint('/api/v1/agent/expert');
|
|
154
|
+
const costSection = (fastCost || expertCost)
|
|
155
|
+
? `\nCOST:\n${fastCost ? ` fast: ${fmtCost(fastCost)}\n` : ''}${expertCost ? ` expert: ${fmtCost(expertCost)}\n` : ''}`
|
|
156
|
+
: '';
|
|
157
|
+
log(`nansen agent — Nansen Research Agent
|
|
158
|
+
|
|
159
|
+
Ask the Nansen AI agent research questions about crypto wallets, tokens,
|
|
160
|
+
smart money flows, and on-chain activity. The agent uses Nansen's full
|
|
161
|
+
data platform to answer your questions.
|
|
162
|
+
|
|
163
|
+
MODES:
|
|
164
|
+
fast Faster responses, best for simple lookups (default)
|
|
165
|
+
expert Deeper analysis, uses a more capable model
|
|
166
|
+
|
|
167
|
+
USAGE:
|
|
168
|
+
nansen agent "<question>"
|
|
169
|
+
nansen agent "<question>" --expert
|
|
170
|
+
nansen agent "<question>" --conversation-id <id>
|
|
171
|
+
|
|
172
|
+
OPTIONS:
|
|
173
|
+
--expert Use expert mode (default: fast)
|
|
174
|
+
--conversation-id <uuid> Continue a previous conversation (UUID v4)
|
|
175
|
+
--json Output raw JSON instead of formatted text
|
|
176
|
+
${costSection}
|
|
177
|
+
CONVERSATION FLOW:
|
|
178
|
+
Each request generates a UUID v4 conversation ID. To continue a
|
|
179
|
+
multi-turn conversation, pass it back with --conversation-id. The ID
|
|
180
|
+
and a ready-to-copy follow-up command are printed to stderr after each
|
|
181
|
+
response.
|
|
182
|
+
|
|
183
|
+
EXAMPLES:
|
|
184
|
+
nansen agent "What are the top smart money inflows on Ethereum today?"
|
|
185
|
+
nansen agent "Show me the largest whale wallets on Solana"
|
|
186
|
+
nansen agent "Analyze wallet 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" --expert
|
|
187
|
+
nansen agent "Tell me more about their DeFi positions" --conversation-id 550e8400-e29b-41d4-a716-446655440000`);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── Parse question ──
|
|
192
|
+
const question = args.join(' ').trim();
|
|
193
|
+
if (!question) {
|
|
194
|
+
throw new NansenError(
|
|
195
|
+
'Query cannot be empty. Usage: nansen agent "<question>"',
|
|
196
|
+
ErrorCode.INVALID_PARAMS,
|
|
197
|
+
null,
|
|
198
|
+
{ detail: 'Empty query string' },
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── Mode ──
|
|
203
|
+
const expert = !!flags.expert;
|
|
204
|
+
const endpoint = expert ? '/api/v1/agent/expert' : '/api/v1/agent/fast';
|
|
205
|
+
const modeName = expert ? 'expert' : 'fast';
|
|
206
|
+
|
|
207
|
+
// ── Conversation ID (must be UUID v4) ──
|
|
208
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
209
|
+
const rawConvId = options['conversation-id'];
|
|
210
|
+
let conversationId;
|
|
211
|
+
if (typeof rawConvId === 'string' && rawConvId) {
|
|
212
|
+
if (!UUID_RE.test(rawConvId)) {
|
|
213
|
+
throw new NansenError(
|
|
214
|
+
`Invalid --conversation-id: expected a UUID (e.g. 550e8400-e29b-41d4-a716-446655440000), got "${rawConvId.slice(0, 60)}${rawConvId.length > 60 ? '...' : ''}"`,
|
|
215
|
+
ErrorCode.INVALID_PARAMS,
|
|
216
|
+
null,
|
|
217
|
+
{ detail: 'conversation-id must be a UUID v4' },
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
conversationId = rawConvId;
|
|
221
|
+
} else {
|
|
222
|
+
conversationId = crypto.randomUUID();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ── Auth guard ──
|
|
226
|
+
if (!apiInstance.apiKey) {
|
|
227
|
+
throw new NansenError(
|
|
228
|
+
'Not logged in. Run: nansen login',
|
|
229
|
+
ErrorCode.UNAUTHORIZED,
|
|
230
|
+
401,
|
|
231
|
+
{ detail: 'No API key configured' },
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ── Request (no retry — SSE streams are not idempotent) ──
|
|
236
|
+
const url = `${apiInstance.baseUrl}${endpoint}`;
|
|
237
|
+
const body = {
|
|
238
|
+
text: question,
|
|
239
|
+
conversation_id: conversationId,
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// ── Timeout ──
|
|
243
|
+
const timeoutMs = expert ? 300_000 : 120_000; // 5min expert, 2min fast
|
|
244
|
+
const controller = new AbortController();
|
|
245
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
246
|
+
|
|
247
|
+
let response;
|
|
248
|
+
try {
|
|
249
|
+
response = await fetch(url, {
|
|
250
|
+
method: 'POST',
|
|
251
|
+
headers: buildHeaders(apiInstance),
|
|
252
|
+
body: JSON.stringify(body),
|
|
253
|
+
signal: controller.signal,
|
|
254
|
+
});
|
|
255
|
+
} catch (err) {
|
|
256
|
+
clearTimeout(timer);
|
|
257
|
+
if (err.name === 'AbortError') {
|
|
258
|
+
throw new NansenError(
|
|
259
|
+
`Request timed out after ${timeoutMs / 1000}s`,
|
|
260
|
+
ErrorCode.TIMEOUT,
|
|
261
|
+
504,
|
|
262
|
+
{ detail: `${modeName} mode timeout (${timeoutMs / 1000}s)` },
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
throw new NansenError(
|
|
266
|
+
`Network error: ${err.message}`,
|
|
267
|
+
ErrorCode.NETWORK_ERROR,
|
|
268
|
+
null,
|
|
269
|
+
{ originalError: err.message },
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (!response.ok) {
|
|
274
|
+
clearTimeout(timer);
|
|
275
|
+
let serverDetail;
|
|
276
|
+
if (response.headers.get('content-type')?.includes('application/json')) {
|
|
277
|
+
try {
|
|
278
|
+
const errData = await response.json();
|
|
279
|
+
serverDetail = errData.detail || errData.message;
|
|
280
|
+
} catch { /* ignore parse failure */ }
|
|
281
|
+
}
|
|
282
|
+
throwApiError(
|
|
283
|
+
serverDetail || `Agent returned ${response.status}`,
|
|
284
|
+
response.status,
|
|
285
|
+
serverDetail,
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ── JSON mode: buffer everything, return structured data ──
|
|
290
|
+
if (flags.json) {
|
|
291
|
+
let result;
|
|
292
|
+
try {
|
|
293
|
+
result = await consumeSSEStream(response);
|
|
294
|
+
} finally {
|
|
295
|
+
clearTimeout(timer);
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
conversation_id: result.conversationId || conversationId,
|
|
299
|
+
mode: modeName,
|
|
300
|
+
text: result.text,
|
|
301
|
+
tool_calls: result.toolCalls,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ── Streaming output mode ──
|
|
306
|
+
let hasOutput = false;
|
|
307
|
+
let midLine = false; // true when write() was called without a trailing newline
|
|
308
|
+
let result;
|
|
309
|
+
try {
|
|
310
|
+
result = await consumeSSEStream(response, {
|
|
311
|
+
onDelta(text) {
|
|
312
|
+
if (!midLine && text.trim() === '') return;
|
|
313
|
+
write(text);
|
|
314
|
+
hasOutput = true;
|
|
315
|
+
midLine = text.length > 0 && !text.endsWith('\n');
|
|
316
|
+
},
|
|
317
|
+
onToolCall(name) {
|
|
318
|
+
if (midLine) { write('\n'); midLine = false; }
|
|
319
|
+
errorLog(`⚙ ${name}`);
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
} finally {
|
|
323
|
+
clearTimeout(timer);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Ensure a trailing newline after streamed text
|
|
327
|
+
if (midLine) {
|
|
328
|
+
write('\n');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (!hasOutput) {
|
|
332
|
+
log('(no response from agent)');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Print conversation continuation hint
|
|
336
|
+
const effectiveConvId = result.conversationId || conversationId;
|
|
337
|
+
const expertFlag = expert ? ' --expert' : '';
|
|
338
|
+
errorLog(`\nTo continue this conversation:`);
|
|
339
|
+
errorLog(` nansen agent "<follow-up>" --conversation-id "${effectiveConvId}"${expertFlag}`);
|
|
340
|
+
|
|
341
|
+
return;
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credit cost cache — reads per-endpoint costs from a local cache populated
|
|
3
|
+
* by an inline fetch of the Nansen OpenAPI spec (at most once per 24h).
|
|
4
|
+
*
|
|
5
|
+
* getCostForEndpoint(endpoint) — sync, reads cache, returns { free, pro } or null
|
|
6
|
+
* refreshCostMapIfStale() — async, fetches inline if cache is missing or stale
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
|
|
12
|
+
const CONFIG_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '', '.nansen');
|
|
13
|
+
const CACHE_FILE = path.join(CONFIG_DIR, 'cost-map.json');
|
|
14
|
+
const STALE_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
15
|
+
const OPENAPI_URL = 'https://api.nansen.ai/openapi.json';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Returns { free, pro } credit cost for the given API path, or null if unavailable.
|
|
19
|
+
*/
|
|
20
|
+
export function getCostForEndpoint(endpoint) {
|
|
21
|
+
try {
|
|
22
|
+
if (!fs.existsSync(CACHE_FILE)) return null;
|
|
23
|
+
const { costs } = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
|
|
24
|
+
return costs?.[endpoint] ?? null;
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Fetches the OpenAPI spec and writes the cost map to disk if the cache is
|
|
32
|
+
* missing or older than 24h. Awaited inline — only blocks on cold/stale cache.
|
|
33
|
+
* Silent on any error.
|
|
34
|
+
*/
|
|
35
|
+
export async function refreshCostMapIfStale() {
|
|
36
|
+
try {
|
|
37
|
+
if (fs.existsSync(CACHE_FILE)) {
|
|
38
|
+
const { fetchedAt } = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
|
|
39
|
+
if (fetchedAt && Date.now() - fetchedAt < STALE_MS) return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const controller = new AbortController();
|
|
43
|
+
const timer = setTimeout(() => controller.abort(), 10_000);
|
|
44
|
+
let spec;
|
|
45
|
+
try {
|
|
46
|
+
const res = await fetch(OPENAPI_URL, { signal: controller.signal });
|
|
47
|
+
spec = await res.json();
|
|
48
|
+
} finally {
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const costs = {};
|
|
53
|
+
for (const [p, methods] of Object.entries(spec.paths || {})) {
|
|
54
|
+
for (const op of Object.values(methods)) {
|
|
55
|
+
if (op['x-credit-cost']) {
|
|
56
|
+
costs[p] = op['x-credit-cost'];
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { mode: 0o700, recursive: true });
|
|
63
|
+
fs.writeFileSync(CACHE_FILE, JSON.stringify({ costs, fetchedAt: Date.now() }));
|
|
64
|
+
} catch {
|
|
65
|
+
// silent — network failure, parse error, write error
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/schema.json
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
"portfolio": {
|
|
7
7
|
"subcommands": {
|
|
8
8
|
"defi": {
|
|
9
|
+
"endpoint": "/api/v1/portfolio/defi-holdings",
|
|
9
10
|
"description": "DeFi holdings across protocols",
|
|
10
11
|
"options": {
|
|
11
12
|
"wallet": {
|
|
@@ -19,6 +20,7 @@
|
|
|
19
20
|
"smart-money": {
|
|
20
21
|
"subcommands": {
|
|
21
22
|
"netflow": {
|
|
23
|
+
"endpoint": "/api/v1/smart-money/netflow",
|
|
22
24
|
"description": "Net capital flows (inflows vs outflows)",
|
|
23
25
|
"options": {
|
|
24
26
|
"chain": {
|
|
@@ -27,6 +29,7 @@
|
|
|
27
29
|
}
|
|
28
30
|
},
|
|
29
31
|
"dex-trades": {
|
|
32
|
+
"endpoint": "/api/v1/smart-money/dex-trades",
|
|
30
33
|
"description": "Real-time DEX trading activity",
|
|
31
34
|
"options": {
|
|
32
35
|
"chain": {
|
|
@@ -35,12 +38,15 @@
|
|
|
35
38
|
}
|
|
36
39
|
},
|
|
37
40
|
"perp-trades": {
|
|
41
|
+
"endpoint": "/api/v1/smart-money/perp-trades",
|
|
38
42
|
"description": "Perpetual trading on Hyperliquid"
|
|
39
43
|
},
|
|
40
44
|
"dcas": {
|
|
45
|
+
"endpoint": "/api/v1/smart-money/dcas",
|
|
41
46
|
"description": "DCA strategies on Jupiter"
|
|
42
47
|
},
|
|
43
48
|
"holdings": {
|
|
49
|
+
"endpoint": "/api/v1/smart-money/holdings",
|
|
44
50
|
"description": "Aggregated token balances",
|
|
45
51
|
"options": {
|
|
46
52
|
"chain": {
|
|
@@ -49,6 +55,7 @@
|
|
|
49
55
|
}
|
|
50
56
|
},
|
|
51
57
|
"historical-holdings": {
|
|
58
|
+
"endpoint": "/api/v1/smart-money/historical-holdings",
|
|
52
59
|
"description": "Historical holdings over time",
|
|
53
60
|
"options": {
|
|
54
61
|
"chain": {
|
|
@@ -65,6 +72,7 @@
|
|
|
65
72
|
"profiler": {
|
|
66
73
|
"subcommands": {
|
|
67
74
|
"transactions": {
|
|
75
|
+
"endpoint": "/api/v1/profiler/address/transactions",
|
|
68
76
|
"description": "Transaction history",
|
|
69
77
|
"options": {
|
|
70
78
|
"address": {
|
|
@@ -79,6 +87,7 @@
|
|
|
79
87
|
}
|
|
80
88
|
},
|
|
81
89
|
"pnl-summary": {
|
|
90
|
+
"endpoint": "/api/v1/profiler/address/pnl-summary",
|
|
82
91
|
"description": "Summarized PnL metrics",
|
|
83
92
|
"options": {
|
|
84
93
|
"address": {
|
|
@@ -93,6 +102,7 @@
|
|
|
93
102
|
}
|
|
94
103
|
},
|
|
95
104
|
"balance": {
|
|
105
|
+
"endpoint": "/api/v1/profiler/address/current-balance",
|
|
96
106
|
"description": "Current token holdings",
|
|
97
107
|
"options": {
|
|
98
108
|
"address": {
|
|
@@ -104,6 +114,7 @@
|
|
|
104
114
|
}
|
|
105
115
|
},
|
|
106
116
|
"counterparties": {
|
|
117
|
+
"endpoint": "/api/v1/profiler/address/counterparties",
|
|
107
118
|
"description": "Top counterparties by volume",
|
|
108
119
|
"options": {
|
|
109
120
|
"address": {
|
|
@@ -118,6 +129,7 @@
|
|
|
118
129
|
}
|
|
119
130
|
},
|
|
120
131
|
"historical-balances": {
|
|
132
|
+
"endpoint": "/api/v1/profiler/address/historical-balances",
|
|
121
133
|
"description": "Historical balances over time",
|
|
122
134
|
"options": {
|
|
123
135
|
"address": {
|
|
@@ -132,6 +144,7 @@
|
|
|
132
144
|
}
|
|
133
145
|
},
|
|
134
146
|
"related-wallets": {
|
|
147
|
+
"endpoint": "/api/v1/profiler/address/related-wallets",
|
|
135
148
|
"description": "Find wallets related to an address",
|
|
136
149
|
"options": {
|
|
137
150
|
"address": {
|
|
@@ -143,6 +156,7 @@
|
|
|
143
156
|
}
|
|
144
157
|
},
|
|
145
158
|
"pnl": {
|
|
159
|
+
"endpoint": "/api/v1/profiler/address/pnl",
|
|
146
160
|
"description": "PnL and trade performance",
|
|
147
161
|
"options": {
|
|
148
162
|
"address": {
|
|
@@ -157,6 +171,7 @@
|
|
|
157
171
|
}
|
|
158
172
|
},
|
|
159
173
|
"labels": {
|
|
174
|
+
"endpoint": "/api/v1/profiler/address/labels",
|
|
160
175
|
"description": "Behavioral and entity labels",
|
|
161
176
|
"options": {
|
|
162
177
|
"address": {
|
|
@@ -168,6 +183,7 @@
|
|
|
168
183
|
}
|
|
169
184
|
},
|
|
170
185
|
"perp-positions": {
|
|
186
|
+
"endpoint": "/api/v1/profiler/perp-positions",
|
|
171
187
|
"description": "Current perpetual positions",
|
|
172
188
|
"options": {
|
|
173
189
|
"address": {
|
|
@@ -176,6 +192,7 @@
|
|
|
176
192
|
}
|
|
177
193
|
},
|
|
178
194
|
"perp-trades": {
|
|
195
|
+
"endpoint": "/api/v1/profiler/perp-trades",
|
|
179
196
|
"description": "Perpetual trading history",
|
|
180
197
|
"options": {
|
|
181
198
|
"address": {
|
|
@@ -187,6 +204,7 @@
|
|
|
187
204
|
}
|
|
188
205
|
},
|
|
189
206
|
"search": {
|
|
207
|
+
"endpoint": "/api/v1/search/entity-name",
|
|
190
208
|
"description": "Search for entities by name",
|
|
191
209
|
"options": {
|
|
192
210
|
"query": {
|
|
@@ -251,6 +269,7 @@
|
|
|
251
269
|
"token": {
|
|
252
270
|
"subcommands": {
|
|
253
271
|
"flows": {
|
|
272
|
+
"endpoint": "/api/v1/tgm/flows",
|
|
254
273
|
"description": "Token flow metrics",
|
|
255
274
|
"options": {
|
|
256
275
|
"chain": {
|
|
@@ -265,6 +284,7 @@
|
|
|
265
284
|
}
|
|
266
285
|
},
|
|
267
286
|
"who-bought-sold": {
|
|
287
|
+
"endpoint": "/api/v1/tgm/who-bought-sold",
|
|
268
288
|
"description": "Recent buyers and sellers",
|
|
269
289
|
"options": {
|
|
270
290
|
"chain": {
|
|
@@ -279,6 +299,7 @@
|
|
|
279
299
|
}
|
|
280
300
|
},
|
|
281
301
|
"dex-trades": {
|
|
302
|
+
"endpoint": "/api/v1/tgm/dex-trades",
|
|
282
303
|
"description": "DEX trading activity",
|
|
283
304
|
"options": {
|
|
284
305
|
"chain": {
|
|
@@ -293,6 +314,7 @@
|
|
|
293
314
|
}
|
|
294
315
|
},
|
|
295
316
|
"transfers": {
|
|
317
|
+
"endpoint": "/api/v1/tgm/transfers",
|
|
296
318
|
"description": "Token transfer history",
|
|
297
319
|
"options": {
|
|
298
320
|
"chain": {
|
|
@@ -307,6 +329,7 @@
|
|
|
307
329
|
}
|
|
308
330
|
},
|
|
309
331
|
"holders": {
|
|
332
|
+
"endpoint": "/api/v1/tgm/holders",
|
|
310
333
|
"description": "Token holder analysis",
|
|
311
334
|
"options": {
|
|
312
335
|
"chain": {
|
|
@@ -318,6 +341,7 @@
|
|
|
318
341
|
}
|
|
319
342
|
},
|
|
320
343
|
"pnl": {
|
|
344
|
+
"endpoint": "/api/v1/tgm/pnl-leaderboard",
|
|
321
345
|
"description": "PnL leaderboard",
|
|
322
346
|
"options": {
|
|
323
347
|
"chain": {
|
|
@@ -332,6 +356,7 @@
|
|
|
332
356
|
}
|
|
333
357
|
},
|
|
334
358
|
"perp-pnl-leaderboard": {
|
|
359
|
+
"endpoint": "/api/v1/tgm/perp-pnl-leaderboard",
|
|
335
360
|
"description": "Perp PnL leaderboard by token",
|
|
336
361
|
"options": {
|
|
337
362
|
"symbol": {
|
|
@@ -343,6 +368,7 @@
|
|
|
343
368
|
}
|
|
344
369
|
},
|
|
345
370
|
"perp-positions": {
|
|
371
|
+
"endpoint": "/api/v1/tgm/perp-positions",
|
|
346
372
|
"description": "Open perp positions by token symbol",
|
|
347
373
|
"options": {
|
|
348
374
|
"symbol": {
|
|
@@ -351,6 +377,7 @@
|
|
|
351
377
|
}
|
|
352
378
|
},
|
|
353
379
|
"perp-trades": {
|
|
380
|
+
"endpoint": "/api/v1/tgm/perp-trades",
|
|
354
381
|
"description": "Perp trades by token symbol",
|
|
355
382
|
"options": {
|
|
356
383
|
"symbol": {
|
|
@@ -362,6 +389,7 @@
|
|
|
362
389
|
}
|
|
363
390
|
},
|
|
364
391
|
"flow-intelligence": {
|
|
392
|
+
"endpoint": "/api/v1/tgm/flow-intelligence",
|
|
365
393
|
"description": "Detailed flow intelligence by label",
|
|
366
394
|
"options": {
|
|
367
395
|
"chain": {
|
|
@@ -376,6 +404,7 @@
|
|
|
376
404
|
}
|
|
377
405
|
},
|
|
378
406
|
"info": {
|
|
407
|
+
"endpoint": "/api/v1/tgm/token-information",
|
|
379
408
|
"description": "Get detailed information for a specific token",
|
|
380
409
|
"options": {
|
|
381
410
|
"chain": {
|
|
@@ -390,6 +419,7 @@
|
|
|
390
419
|
}
|
|
391
420
|
},
|
|
392
421
|
"indicators": {
|
|
422
|
+
"endpoint": "/api/v1/tgm/indicators",
|
|
393
423
|
"description": "Risk and reward indicators for a token (Nansen Score)",
|
|
394
424
|
"options": {
|
|
395
425
|
"chain": {
|
|
@@ -401,6 +431,7 @@
|
|
|
401
431
|
}
|
|
402
432
|
},
|
|
403
433
|
"ohlcv": {
|
|
434
|
+
"endpoint": "/api/v1/tgm/token-ohlcv",
|
|
404
435
|
"description": "OHLCV candle data for a token",
|
|
405
436
|
"options": {
|
|
406
437
|
"chain": {
|
|
@@ -415,6 +446,7 @@
|
|
|
415
446
|
}
|
|
416
447
|
},
|
|
417
448
|
"jup-dca": {
|
|
449
|
+
"endpoint": "/api/v1/tgm/jup-dca",
|
|
418
450
|
"description": "Jupiter DCA orders for token",
|
|
419
451
|
"options": {
|
|
420
452
|
"token": {
|
|
@@ -423,6 +455,7 @@
|
|
|
423
455
|
}
|
|
424
456
|
},
|
|
425
457
|
"screener": {
|
|
458
|
+
"endpoint": "/api/v1/token-screener",
|
|
426
459
|
"description": "Discover and filter tokens",
|
|
427
460
|
"options": {
|
|
428
461
|
"timeframe": {
|
|
@@ -437,6 +470,7 @@
|
|
|
437
470
|
"description": "Token God Mode - deep analytics for any token"
|
|
438
471
|
},
|
|
439
472
|
"search": {
|
|
473
|
+
"endpoint": "/api/v1/search/general",
|
|
440
474
|
"description": "Search for tokens and entities across Nansen",
|
|
441
475
|
"options": {
|
|
442
476
|
"query": {
|
|
@@ -453,6 +487,7 @@
|
|
|
453
487
|
"perp": {
|
|
454
488
|
"subcommands": {
|
|
455
489
|
"screener": {
|
|
490
|
+
"endpoint": "/api/v1/perp-screener",
|
|
456
491
|
"description": "Screen perpetual futures contracts",
|
|
457
492
|
"options": {
|
|
458
493
|
"days": {
|
|
@@ -461,6 +496,7 @@
|
|
|
461
496
|
}
|
|
462
497
|
},
|
|
463
498
|
"leaderboard": {
|
|
499
|
+
"endpoint": "/api/v1/perp-leaderboard",
|
|
464
500
|
"description": "Perpetual futures PnL leaderboard",
|
|
465
501
|
"options": {
|
|
466
502
|
"days": {
|
|
@@ -474,6 +510,7 @@
|
|
|
474
510
|
"prediction-market": {
|
|
475
511
|
"subcommands": {
|
|
476
512
|
"ohlcv": {
|
|
513
|
+
"endpoint": "/api/v1/prediction-market/ohlcv",
|
|
477
514
|
"description": "Get Prediction Market OHLCV Candles",
|
|
478
515
|
"options": {
|
|
479
516
|
"market-id": {
|
|
@@ -482,6 +519,7 @@
|
|
|
482
519
|
}
|
|
483
520
|
},
|
|
484
521
|
"orderbook": {
|
|
522
|
+
"endpoint": "/api/v1/prediction-market/orderbook",
|
|
485
523
|
"description": "Get Prediction Market Orderbook",
|
|
486
524
|
"options": {
|
|
487
525
|
"market-id": {
|
|
@@ -490,6 +528,7 @@
|
|
|
490
528
|
}
|
|
491
529
|
},
|
|
492
530
|
"top-holders": {
|
|
531
|
+
"endpoint": "/api/v1/prediction-market/top-holders",
|
|
493
532
|
"description": "Get Prediction Market Top Holders",
|
|
494
533
|
"options": {
|
|
495
534
|
"market-id": {
|
|
@@ -498,6 +537,7 @@
|
|
|
498
537
|
}
|
|
499
538
|
},
|
|
500
539
|
"trades-by-market": {
|
|
540
|
+
"endpoint": "/api/v1/prediction-market/trades-by-market",
|
|
501
541
|
"description": "Get Prediction Market Trades by Market",
|
|
502
542
|
"options": {
|
|
503
543
|
"market-id": {
|
|
@@ -506,6 +546,7 @@
|
|
|
506
546
|
}
|
|
507
547
|
},
|
|
508
548
|
"market-screener": {
|
|
549
|
+
"endpoint": "/api/v1/prediction-market/market-screener",
|
|
509
550
|
"description": "Get Prediction Market Screener",
|
|
510
551
|
"options": {
|
|
511
552
|
"query": {
|
|
@@ -514,6 +555,7 @@
|
|
|
514
555
|
}
|
|
515
556
|
},
|
|
516
557
|
"event-screener": {
|
|
558
|
+
"endpoint": "/api/v1/prediction-market/event-screener",
|
|
517
559
|
"description": "Get Prediction Market Event Screener",
|
|
518
560
|
"options": {
|
|
519
561
|
"query": {
|
|
@@ -522,6 +564,7 @@
|
|
|
522
564
|
}
|
|
523
565
|
},
|
|
524
566
|
"pnl-by-market": {
|
|
567
|
+
"endpoint": "/api/v1/prediction-market/pnl-by-market",
|
|
525
568
|
"description": "Get Prediction Market PnL by Market",
|
|
526
569
|
"options": {
|
|
527
570
|
"market-id": {
|
|
@@ -530,6 +573,7 @@
|
|
|
530
573
|
}
|
|
531
574
|
},
|
|
532
575
|
"pnl-by-address": {
|
|
576
|
+
"endpoint": "/api/v1/prediction-market/pnl-by-address",
|
|
533
577
|
"description": "Get Prediction Market PnL by Address",
|
|
534
578
|
"options": {
|
|
535
579
|
"address": {
|
|
@@ -538,6 +582,7 @@
|
|
|
538
582
|
}
|
|
539
583
|
},
|
|
540
584
|
"position-detail": {
|
|
585
|
+
"endpoint": "/api/v1/prediction-market/position-detail",
|
|
541
586
|
"description": "Get Prediction Market Position Detail",
|
|
542
587
|
"options": {
|
|
543
588
|
"market-id": {
|
|
@@ -546,6 +591,7 @@
|
|
|
546
591
|
}
|
|
547
592
|
},
|
|
548
593
|
"trades-by-address": {
|
|
594
|
+
"endpoint": "/api/v1/prediction-market/trades-by-address",
|
|
549
595
|
"description": "Get Prediction Market Trades by Address",
|
|
550
596
|
"options": {
|
|
551
597
|
"address": {
|
|
@@ -554,6 +600,7 @@
|
|
|
554
600
|
}
|
|
555
601
|
},
|
|
556
602
|
"categories": {
|
|
603
|
+
"endpoint": "/api/v1/prediction-market/categories",
|
|
557
604
|
"description": "Get Prediction Market Categories"
|
|
558
605
|
}
|
|
559
606
|
},
|
|
@@ -826,6 +873,35 @@
|
|
|
826
873
|
]
|
|
827
874
|
}
|
|
828
875
|
}
|
|
876
|
+
},
|
|
877
|
+
"agent": {
|
|
878
|
+
"description": "Nansen AI research agent \u2014 ask questions about wallets, tokens, and on-chain activity",
|
|
879
|
+
"options": {
|
|
880
|
+
"expert": {
|
|
881
|
+
"type": "boolean",
|
|
882
|
+
"description": "Use expert mode (deeper analysis, slower). Default: fast mode."
|
|
883
|
+
},
|
|
884
|
+
"conversation-id": {
|
|
885
|
+
"type": "string",
|
|
886
|
+
"format": "uuid",
|
|
887
|
+
"description": "Continue a previous conversation by passing its UUID v4 ID"
|
|
888
|
+
},
|
|
889
|
+
"json": {
|
|
890
|
+
"type": "boolean",
|
|
891
|
+
"description": "Output raw JSON (conversation_id, mode, text, tool_calls) instead of formatted text"
|
|
892
|
+
}
|
|
893
|
+
},
|
|
894
|
+
"examples": [
|
|
895
|
+
"nansen agent \"What are the top smart money inflows on Ethereum today?\"",
|
|
896
|
+
"nansen agent \"Analyze wallet 0xd8dA...6045\" --expert",
|
|
897
|
+
"nansen agent \"Tell me more\" --conversation-id <id>"
|
|
898
|
+
],
|
|
899
|
+
"returns": [
|
|
900
|
+
"conversation_id",
|
|
901
|
+
"mode",
|
|
902
|
+
"text",
|
|
903
|
+
"tool_calls"
|
|
904
|
+
]
|
|
829
905
|
}
|
|
830
906
|
},
|
|
831
907
|
"globalOptions": {
|
package/src/wallet.js
CHANGED
|
@@ -596,8 +596,15 @@ export function buildWalletCommands(deps = {}) {
|
|
|
596
596
|
// All other subcommands fall through to unified handlers below
|
|
597
597
|
}
|
|
598
598
|
|
|
599
|
+
const isPrivy = options.provider === 'privy' || process.env.NANSEN_WALLET_PROVIDER === 'privy';
|
|
600
|
+
|
|
599
601
|
const handlers = {
|
|
600
602
|
'create': async () => {
|
|
603
|
+
// Privy wallets are handled above — if we reach here with provider=privy,
|
|
604
|
+
// the privy path already failed and called exit(). Guard against environments
|
|
605
|
+
// where exit() does not terminate (agent frameworks, test harnesses).
|
|
606
|
+
if (isPrivy) return;
|
|
607
|
+
|
|
601
608
|
const name = options.name || args[1] || 'default';
|
|
602
609
|
|
|
603
610
|
let password;
|