From 994e4e046b4cdb1188ade6f94397ac5c30b3d714 Mon Sep 17 00:00:00 2001 From: "Ko Miyatake (Openclaw Bot)" Date: Tue, 2 Jun 2026 08:22:20 +0000 Subject: [PATCH 1/2] feat: add profiler dex-trades command Adds `nansen research profiler dex-trades` to surface DEX trade history for a wallet address on any supported chain via POST /api/v1/profiler/dex-trades. Supports --address, --chain, --days, --date, --sort, --page, --limit, and --filter flags, consistent with other profiler subcommands. Co-Authored-By: Claude Sonnet 4.6 --- src/__tests__/api.test.js | 119 ++++++++++++++++++++++++++++++++- src/__tests__/coverage.test.js | 1 + src/api.js | 14 ++++ src/cli.js | 6 +- src/schema.json | 15 +++++ 5 files changed, 152 insertions(+), 3 deletions(-) diff --git a/src/__tests__/api.test.js b/src/__tests__/api.test.js index a0df6dcf..7d44da0f 100644 --- a/src/__tests__/api.test.js +++ b/src/__tests__/api.test.js @@ -149,6 +149,33 @@ const MOCK_RESPONSES = { { token: 'ETH', side: 'short', pnl_usd: 5000 } ] }, + addressDexTrades: { + data: [ + { + chain: 'ethereum', + block_timestamp: '2024-01-15T10:30:00', + transaction_hash: '0xabc123', + trader_address: '0x28c6c06298d514db089934071355e5743bf21d60', + trader_address_label: 'Smart Trader', + token_bought_address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', + token_sold_address: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', + token_bought_amount: 1000.0, + token_sold_amount: 1.5, + token_bought_symbol: 'USDC', + token_sold_symbol: 'ETH', + token_bought_age_days: 365, + token_sold_age_days: 730, + token_bought_market_cap: 50000000000.0, + token_sold_market_cap: 200000000000.0, + token_bought_fdv: 55000000000.0, + token_sold_fdv: 220000000000.0, + trade_value_usd: 3000.0 + } + ], + page: 1, + per_page: 100, + total: 1 + }, tokenIndicators: { token_address: '0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9', chain: 'ethereum', @@ -1058,12 +1085,12 @@ describe('NansenAPI', () => { it('should calculate correct date range for custom days', async () => { setupMock(MOCK_RESPONSES.addressPerpTrades); - + await api.addressPerpTrades({ address: TEST_DATA.ethereum.address, days: 7 }); - + const body = expectFetchCalledWith('/api/v1/profiler/perp-trades'); const from = new Date(body.date.from); const to = new Date(body.date.to); @@ -1071,6 +1098,94 @@ describe('NansenAPI', () => { expect(diffDays).toBe(7); }); }); + + describe('addressDexTrades', () => { + it('should fetch dex trades with correct endpoint', async () => { + setupMock(MOCK_RESPONSES.addressDexTrades); + + const result = await api.addressDexTrades({ + address: TEST_DATA.ethereum.address, + chain: 'ethereum' + }); + + const body = expectFetchCalledWith('/api/v1/profiler/dex-trades'); + expect(body.address).toBe(TEST_DATA.ethereum.address); + expect(body.chain).toBe('ethereum'); + + expect(result.data).toBeInstanceOf(Array); + expect(result.data[0]).toHaveProperty('token_bought_symbol', 'USDC'); + expect(result.data[0]).toHaveProperty('trade_value_usd', 3000.0); + }); + + it('should pass orderBy to API', async () => { + setupMock(MOCK_RESPONSES.addressDexTrades); + + await api.addressDexTrades({ + address: TEST_DATA.ethereum.address, + chain: 'ethereum', + orderBy: [{ column: 'block_timestamp', order: 'desc' }] + }); + + const body = expectFetchCalledWith('/api/v1/profiler/dex-trades'); + expect(body.order_by).toEqual([{ column: 'block_timestamp', order: 'desc' }]); + }); + + it('should include date range with default days', async () => { + setupMock(MOCK_RESPONSES.addressDexTrades); + + await api.addressDexTrades({ + address: TEST_DATA.ethereum.address, + chain: 'ethereum' + }); + + const body = expectFetchCalledWith('/api/v1/profiler/dex-trades'); + expect(body.date).toBeDefined(); + expect(body.date.from).toBeDefined(); + expect(body.date.to).toBeDefined(); + }); + + it('should calculate correct date range for custom days', async () => { + setupMock(MOCK_RESPONSES.addressDexTrades); + + await api.addressDexTrades({ + address: TEST_DATA.ethereum.address, + chain: 'ethereum', + days: 14 + }); + + const body = expectFetchCalledWith('/api/v1/profiler/dex-trades'); + const from = new Date(body.date.from); + const to = new Date(body.date.to); + const diffDays = Math.round((to - from) / (1000 * 60 * 60 * 24)); + expect(diffDays).toBe(14); + }); + + it('should pass filters through', async () => { + setupMock(MOCK_RESPONSES.addressDexTrades); + + await api.addressDexTrades({ + address: TEST_DATA.ethereum.address, + chain: 'ethereum', + filters: { min_trade_value_usd: 1000 } + }); + + const body = expectFetchCalledWith('/api/v1/profiler/dex-trades'); + expect(body.filters.min_trade_value_usd).toBe(1000); + }); + + it('should work with solana address', async () => { + setupMock(MOCK_RESPONSES.addressDexTrades); + + await api.addressDexTrades({ + address: TEST_DATA.solana.address, + chain: 'solana' + }); + + const body = expectFetchCalledWith('/api/v1/profiler/dex-trades'); + expect(body.address).toBe(TEST_DATA.solana.address); + expect(body.chain).toBe('solana'); + }); + }); }); // =================== Token God Mode Endpoints =================== diff --git a/src/__tests__/coverage.test.js b/src/__tests__/coverage.test.js index c8937a27..dc2af6a6 100644 --- a/src/__tests__/coverage.test.js +++ b/src/__tests__/coverage.test.js @@ -30,6 +30,7 @@ const DOCUMENTED_ENDPOINTS = { { name: 'pnl-summary', method: 'addressPnlSummary', endpoint: '/api/v1/profiler/address/pnl-summary' }, { name: 'perp-positions', method: 'addressPerpPositions', endpoint: '/api/v1/profiler/perp-positions' }, { name: 'perp-trades', method: 'addressPerpTrades', endpoint: '/api/v1/profiler/perp-trades' }, + { name: 'dex-trades', method: 'addressDexTrades', endpoint: '/api/v1/profiler/dex-trades' }, ], tokenGodMode: [ { name: 'indicators', method: 'tokenIndicators', endpoint: '/api/v1/tgm/indicators' }, diff --git a/src/api.js b/src/api.js index a0f968a3..37b3da97 100644 --- a/src/api.js +++ b/src/api.js @@ -973,6 +973,20 @@ export class NansenAPI { }); } + async addressDexTrades(params = {}) { + const { address, chain = 'ethereum', filters = {}, orderBy, pagination, days = 30, date } = params; + if (address) requireValidAddress(address, chain); + const dateRange = date || buildDateRange(days); + return this.request('/api/v1/profiler/dex-trades', { + address, + chain, + date: dateRange, + filters, + order_by: orderBy, + pagination + }); + } + // ============= Token God Mode Endpoints ============= async tokenScreener(params = {}) { diff --git a/src/cli.js b/src/cli.js index 3042f399..55585a15 100644 --- a/src/cli.js +++ b/src/cli.js @@ -1148,6 +1148,10 @@ export function buildCommands(deps = {}) { 'pnl-summary': () => apiInstance.addressPnlSummary({ address, chain, orderBy, pagination, days }), 'perp-positions': () => apiInstance.addressPerpPositions({ address, filters, orderBy, pagination }), 'perp-trades': () => apiInstance.addressPerpTrades({ address, filters, orderBy, pagination, days }), + 'dex-trades': () => { + const date = parseDateOption(options.date, days); + return apiInstance.addressDexTrades({ address, chain, filters, orderBy, pagination, days, date }); + }, 'batch': () => { let addresses = []; if (options.addresses) { @@ -1186,7 +1190,7 @@ export function buildCommands(deps = {}) { return compareWallets(apiInstance, { addresses: addrs, chain, days }); }, 'help': () => ({ - commands: ['balance', 'labels', 'transactions', 'pnl', 'search', 'historical-balances', 'related-wallets', 'counterparties', 'pnl-summary', 'perp-positions', 'perp-trades', 'batch', 'trace', 'compare'], + commands: ['balance', 'labels', 'transactions', 'pnl', 'search', 'historical-balances', 'related-wallets', 'counterparties', 'pnl-summary', 'perp-positions', 'perp-trades', 'dex-trades', 'batch', 'trace', 'compare'], description: 'Wallet profiling endpoints', example: 'nansen research profiler compare --addresses "0xABC...,0xDEF..." --chain ethereum' }) diff --git a/src/schema.json b/src/schema.json index 965efae7..8931483f 100644 --- a/src/schema.json +++ b/src/schema.json @@ -203,6 +203,21 @@ } } }, + "dex-trades": { + "endpoint": "/api/v1/profiler/dex-trades", + "description": "DEX trade history for a wallet", + "options": { + "address": { + "required": true + }, + "chain": { + "default": "ethereum" + }, + "days": { + "default": 30 + } + } + }, "search": { "endpoint": "/api/v1/search/entity-name", "description": "Search for entities by name", From 73600d9cd13248a265332ec0728c602b16f44868 Mon Sep 17 00:00:00 2001 From: "Ko Miyatake (Openclaw Bot)" Date: Tue, 2 Jun 2026 08:22:35 +0000 Subject: [PATCH 2/2] chore: add changeset for profiler dex-trades --- .changeset/profiler-dex-trades.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/profiler-dex-trades.md diff --git a/.changeset/profiler-dex-trades.md b/.changeset/profiler-dex-trades.md new file mode 100644 index 00000000..2a244518 --- /dev/null +++ b/.changeset/profiler-dex-trades.md @@ -0,0 +1,5 @@ +--- +"nansen-cli": minor +--- + +Add `nansen research profiler dex-trades` command for DEX trade history