🧪 Skills

nadfunagent

Autonomous Nad.fun trading agent that scans markets via API and indexer, analyzes tokens, executes trades, and shares profits with MMIND token holders.

v1.0.0
❤️ 0
⬇️ 462
👁 1
Share

Description


name: nadfunagent description: Autonomous trading agent for Nad.fun that scans markets, analyzes tokens, executes trades, and distributes profits to MMIND token holders. Uses nadfun-trading, nadfun-indexer, and nadfun-agent-api skills. user-invocable: true argument-hint: [action] [options]

CRITICAL COMMUNICATION RULES:

  1. Language: Always respond in the SAME language as the user's question. If in English, respond in English.
  2. Data Loading: BEFORE executing any operations, FIRST request and load ALL required data from user or configuration files. Use OpenClaw's memory/session storage to save loaded data so you don't need to ask again.
  3. Telegram Integration:
    • Send detailed reports to Telegram after each trading cycle
    • If Telegram bot connection is not established, request user to start conversation with bot first
    • Include position status, P&L, new opportunities found, trades executed
  4. Initial Setup: When first invoked, ask user for:
    • MMIND_TOKEN_ADDRESS (or load from .env)
    • MONAD_PRIVATE_KEY (or load from .env)
    • MONAD_RPC_URL (or load from .env)
    • MONAD_NETWORK (or load from .env)
    • Telegram user ID for notifications Save all this data in OpenClaw memory for future use.
  5. Missing variables: If .env is missing or any required variable (MONAD_PRIVATE_KEY, MONAD_RPC_URL, MMIND_TOKEN_ADDRESS, MONAD_NETWORK) is not set, ask the user to provide it before running trading or scripts. Do not proceed with buy/sell or execute-bonding-v2 until config is complete.

Nad.fun Autonomous Trading Agent

Autonomous trading agent that scans Nad.fun markets, analyzes tokens using momentum strategies, executes trades, and distributes profits to MMIND token holders.

Prerequisites

  • monad-development skill installed (for wallet and RPC setup)
  • nadfun-trading skill installed (for buy/sell operations), or use the trading/ folder from this repo
  • nadfun-indexer skill installed (for querying events)
  • nadfun-agent-api skill installed (for market data)
  • Network configured (mainnet only for this skill)
  • MMIND token address configured

Paths (clean install): Config is read from NADFUN_ENV_PATH if set, else $HOME/nadfunagent/.env. Positions report: POSITIONS_REPORT_PATH or $HOME/nadfunagent/positions_report.json. Scripts from nadfun-trading may be at ~/.openclaw/workspace/skills/nadfun-trading or at <this-repo>/trading. See DEPENDENCIES.md.

Configuration

CRITICAL: Load environment variables from .env file (default path: $HOME/nadfunagent/.env; override with NADFUN_ENV_PATH):

  • MMIND_TOKEN_ADDRESS: Address of MMIND token for profit distribution (required)
  • MONAD_PRIVATE_KEY: Private key for trading wallet (required)
  • MONAD_RPC_URL: RPC endpoint URL (required)
  • MONAD_NETWORK: Network type - "mainnet" or "testnet" (required)
  • MAX_POSITION_SIZE: Maximum position size in MON (default: 0.1)
  • PROFIT_TARGET_PERCENT: Take-profit threshold (default: 20%)
  • STOP_LOSS_PERCENT: Stop-loss threshold (default: 10%)
  • PNL_DISTRIBUTION_PERCENT: % of profit to distribute (default: 30%)

Before starting trading cycle:

  1. Read .env file (path: NADFUN_ENV_PATH or $HOME/nadfunagent/.env). If the file is missing or any required variable is empty, ask the user to provide MMIND_TOKEN_ADDRESS, MONAD_PRIVATE_KEY, MONAD_RPC_URL, MONAD_NETWORK — do not run trading until config is complete.
  2. Load MMIND_TOKEN_ADDRESS - use this for profit distribution
  3. Load MONAD_PRIVATE_KEY - use this for wallet operations
  4. Load MONAD_RPC_URL - use this for blockchain queries
  5. Load MONAD_NETWORK - determines API endpoints (mainnet: api.nadapp.net, testnet: dev-api.nad.fun)

Workflow

EXECUTION SUMMARY - READ THIS FIRST:

  1. Load config from $HOME/nadfunagent/.env (MMIND_TOKEN_ADDRESS, MONAD_PRIVATE_KEY, MONAD_RPC_URL, MONAD_NETWORK)
  2. Execute ONLY 3 methods (from HAR file analysis): Method 5 (New Events API), Method 6 (Market Cap API), Method 7 (Creation Time API)
  3. Combine results - merge all token addresses from 3 methods, count frequency (how many methods found each token, max 3)
  4. Prioritize - sort tokens by frequency (tokens found in 2-3 methods = higher priority)
  5. Save tokens - save ALL found tokens (up to top 50 for storage) to $HOME/nadfunagent/found_tokens.json for manual review
  6. Analyze - get token info, market data, metrics via Agent API, calculate scores
  7. Filter - keep only tokens with min liquidity 5 MON, min holders 5
  8. Trade - execute trades if score >= 60
  9. Distribute - distribute profits to MMIND holders if profit >= 0.1 MON

1. Market Scanning (NEW + TRENDING)

CRITICAL INSTRUCTIONS: You MUST scan tokens using ALL 7 methods below. Execute each method step-by-step and collect ALL found token addresses.

EXECUTION ORDER:

  1. Method 1: CurveCreate events (Indexer) - NEW tokens
  2. Method 2: CurveBuy events (Indexer) - TRENDING by volume
  3. Method 3: Swap History (Agent API) - TRENDING by 24h volume
  4. Method 4: Holdings (Agent API) - ACTIVE tokens from large traders
  5. Method 5: New Events API - REAL-TIME BUY/CREATE events
  6. Method 6: Market Cap API - TOP tokens by capitalization
  7. Method 7: Creation Time API - NEWEST tokens

STEP-BY-STEP EXECUTION:

CRITICAL LOGGING REQUIREMENT: After executing EACH method, you MUST log:

  1. Print: "✅ Method X executed: Found N tokens"
  2. Print: "Method X token addresses: ..."
  3. If method failed: Print: "❌ Method X failed: "
  4. This helps track which methods are working and debug issues

NOTE: Only Methods 5, 6, and 7 are used (from HAR file analysis). Methods 1-4 are disabled.

STATUS CHECK: This method requires RPC access. You MUST execute it, don't skip it!

What to do:

  1. Read $HOME/nadfunagent/.env to get MONAD_RPC_URL and MONAD_NETWORK
  2. Get current block number using RPC: eth_blockNumber
  3. Calculate: safeLatest = currentBlock - 10 (safety margin)
  4. Scan last 900 blocks in chunks of 100 blocks (RPC limit)
  5. For each chunk, query CurveCreate events using nadfun-indexer skill

How to execute:

  • Use nadfun-indexer skill with parameters: event=CurveCreate, fromBlock=<start>, toBlock=<end>
  • Extract token addresses from events: event.args.token or event.args[1] (token is second argument)
  • Collect all unique token addresses

Example command structure:

Use skill: nadfun-indexer
Parameters: event=CurveCreate, fromBlock=<calculated_start>, toBlock=<calculated_end>
Extract: token address from each event

Expected result: Array of token addresses (0x...) from newly created tokens

Method 2: Trending Tokens via Indexer (CurveBuy volume analysis)

STATUS CHECK: This method requires RPC access. You MUST execute it, don't skip it!

What to do:

  1. Query CurveBuy events from last 900 blocks (same pagination as Method 1)
  2. For each event, extract: token address and amountIn (MON spent)
  3. Sum volume per token address
  4. Sort tokens by total volume (descending)
  5. Take top 20 tokens with highest volume

How to execute:

  1. Use same RPC setup as Method 1: Get publicClient from monad-development skill
  2. Query CurveBuy events: For each 100-block chunk:
    • Use nadfun-indexer skill: event=CurveBuy, fromBlock=<start>, toBlock=<end>
    • OR use viem: publicClient.getContractEvents({ address: CURVE_ADDRESS, eventName: "CurveBuy", fromBlock, toBlock })
  3. Extract data: From each event:
    • token = event.args.token or event.args[1]
    • amountIn = event.args.amountIn or event.args[2] (amount in MON)
  4. Calculate volume: Group by token address, sum all amountIn values
  5. Sort and filter: Sort by total volume DESC, take top 20
  6. Log results: Print "✅ Method 2: Found top 20 trending tokens by volume"

Example:

1. Use same RPC client from Method 1
2. Query CurveBuy events in chunks of 100 blocks
3. For each event: extract token and amountIn
4. Sum volume per token
5. Sort DESC, take top 20
6. Print: "✅ Method 2: Found 20 trending tokens"

Expected result: Top 20 token addresses sorted by trading volume

Method 3: Trending Tokens via Agent API (Swap History Analysis)

What to do:

  1. Take top 10 tokens from Method 2 (highest volume)
  2. For each token, query swap history via Agent API
  3. Calculate 24h volume from swap history
  4. Keep tokens with >= 1 MON volume in last 24 hours

How to execute:

  • Determine API URL: if MONAD_NETWORK=mainnet then https://api.nadapp.net, else https://dev-api.nad.fun
  • For each token, make GET request: ${API_URL}/agent/swap-history/${tokenAddress}?limit=50&trade_type=ALL
  • Parse response JSON
  • For each swap in response.swaps:
    • Check swap.swap_info.timestamp - if within last 24 hours
    • Sum swap.swap_info.native_amount (in wei, convert to MON: divide by 1e18)
  • Keep tokens with total 24h volume >= 1 MON
  • Add 1-2 second delay between API calls to avoid rate limits

Example:

API_URL = MONAD_NETWORK === 'mainnet' ? 'https://api.nadapp.net' : 'https://dev-api.nad.fun'
For each token in top10:
  GET ${API_URL}/agent/swap-history/${token}?limit=50&trade_type=ALL
  Calculate 24h volume
  If volume >= 1 MON: add to trendingViaAPI
  Wait 1-2 seconds

Expected result: List of tokens with high 24h trading volume

Method 4: Get All Active Tokens (via Holdings of Large Accounts)

STATUS CHECK: Don't skip this method! Execute it with rate limit delays.

What to do:

  1. From Method 2 CurveBuy events, find buyers who spent >1 MON
  2. Extract their addresses (sender field)
  3. Query holdings for first 5 large buyers (to avoid rate limits)
  4. Extract all token addresses they hold (balance > 0)

How to execute:

  1. From Method 2 results: Use CurveBuy events already queried
  2. Filter large buyers:
    • Filter events where amountIn > 1000000000000000000 (1 MON in wei = 1e18)
    • Extract sender address: event.args.sender or event.args[0]
    • Get unique addresses, take first 5
  3. Query holdings: For each of 5 addresses:
    • GET: ${API_URL}/agent/holdings/${address}?limit=50
    • Headers: Accept: application/json
    • Wait 2 seconds between requests
  4. Parse and extract:
    • Parse JSON response
    • For each holding in response.tokens:
      • Check holding.balance_info.balance - if > 0
      • Extract holding.token_info.address or holding.token_info.token_id
  5. Log results: Print "✅ Method 4: Found N active tokens from large trader holdings"

Example:

From Method 2 CurveBuy events:
  Filter: amountIn > 1e18 (1 MON)
  Extract: sender addresses
  Take first 5 unique addresses
For each address:
  GET ${API_URL}/agent/holdings/${address}?limit=50
  Extract tokens with balance > 0
  Wait 2 seconds
Print: "✅ Method 4: Found X tokens"

Expected result: Set of token addresses held by active large traders

Method 5: New Events API (Real-time BUY/SELL/CREATE events)

STATUS: ✅ This method is WORKING! Found 7 tokens in last scan.

What to do:

  1. Make GET request to /api/token/new-event endpoint
  2. Parse JSON response (array of events)
  3. Extract token addresses from CREATE and BUY events
  4. Add all unique token addresses to collection

How to execute:

  1. Determine base URL: Read MONAD_NETWORK from .env:
    • If MONAD_NETWORK=mainnet: baseUrl = 'https://nad.fun'
    • Else: baseUrl = 'https://dev.nad.fun'
  2. Make GET request:
    • URL: ${baseUrl}/api/token/new-event
    • Headers: Accept: application/json, User-Agent: OpenClaw-Agent/1.0
  3. Parse response: JSON array of event objects
  4. Extract tokens:
    • For each event in response:
      • If event.type === 'CREATE': extract event.token_info.token_id
      • If event.type === 'BUY': extract event.token_info.token_id
      • If event.type === 'SELL': optionally extract (indicates active trading)
  5. Log results: Print "✅ Method 5: Found N tokens from new events API"

Example:

baseUrl = MONAD_NETWORK === 'mainnet' ? 'https://nad.fun' : 'https://dev.nad.fun'
GET ${baseUrl}/api/token/new-event
Parse JSON response
For each event:
  If event.type === 'CREATE' or 'BUY':
    Add event.token_info.token_id to tokensFromEvents
Print: "✅ Method 5: Found X tokens"

Expected result: Array of token addresses from recent CREATE and BUY events

Response structure:

  • type: "BUY" | "SELL" | "CREATE"
  • amount: Event amount (string)
  • token_info: Complete token information (token_id, name, symbol, description, creator, etc.)
  • account_info: Account that performed the action

Method 6: Market Cap API (Top tokens by market cap)

STATUS CHECK: Previous error: base64 decoding failed. Fix: Use proper decoding method.

What to do:

  1. GET request to market cap endpoint
  2. Response is base64-encoded JSON - decode it PROPERLY
  3. Extract token addresses from response
  4. Add all token addresses to collection

How to execute:

  1. API URL: ${API_URL}/order/market_cap?page=1&limit=50&is_nsfw=false
    • Where API_URL = MONAD_NETWORK === 'mainnet' ? 'https://api.nadapp.net' : 'https://dev-api.nad.fun'
  2. GET request:
    • Headers: Accept: application/json, User-Agent: OpenClaw-Agent/1.0
    • Get response as text (not JSON)
  3. Decode base64 PROPERLY:
    • Response body is base64 string
    • Use Python: import base64; decoded = base64.b64decode(response_text).decode('utf-8')
    • OR use command: echo "$response_text" | base64 -d
    • CRITICAL: Make sure response_text is the raw base64 string, not wrapped in JSON
  4. Parse decoded JSON:
    • Parse decoded string as JSON
    • Structure: { "tokens": [...], "total_count": N }
  5. Extract tokens WITH FULL DATA:
    • For each token in data.tokens:
      • Extract token.token_info.token_id (token address)
      • CRITICAL: Store FULL token object with both token_info AND market_info
      • Structure: {token_info: {...}, market_info: {...}, percent: ...}
      • Add to topMarketCapTokens array (store full objects, not just addresses)
  6. Log results: Print "✅ Method 6: Found N tokens with market data from market cap API"

Example:

API_URL = MONAD_NETWORK === 'mainnet' ? 'https://api.nadapp.net' : 'https://dev-api.nad.fun'
response = GET ${API_URL}/order/market_cap?page=1&limit=100&is_nsfw=false
decoded = base64.b64decode(response.text).decode('utf-8')
data = json.loads(decoded)

topMarketCapTokens = []  # Array of full token objects
For each token in data.tokens:
  # Store FULL token object with token_info + market_info
  topMarketCapTokens.append({
    'token_info': token.token_info,
    'market_info': token.market_info,
    'percent': token.percent,
    'address': token.token_info.token_id  # For easy access
  })
Print: "✅ Method 6: Found X tokens with market data"

Expected result: Array of full token objects (with token_info + market_info) sorted by market cap

Response structure:

  • tokens: Array of token objects with token_info and market_info
  • total_count: Total number of tokens
  • Each token includes: token_info (token_id, name, symbol, creator, etc.) and market_info (market_type, price, volume, holder_count, etc.)

Method 7: Creation Time API (Newest tokens)

CRITICAL: Include BOTH bonding curve AND DEX tokens! Do NOT filter by is_graduated.

What to do:

  1. GET request to creation time endpoint (newest first)
  2. Decode base64 response PROPERLY (same as Method 6)
  3. Extract token addresses - include ALL tokens (both bonding curve AND DEX)
  4. Add to collection WITH FULL DATA

How to execute:

  • API URL: ${API_URL}/order/creation_time?page=1&limit=50&is_nsfw=false&direction=DESC
  • GET request, decode base64 response (same as Method 6)
  • Parse JSON: data.tokens array
  • For each token:
    • Extract token.token_info.token_id (token address)
    • CRITICAL: Store FULL token object with both token_info AND market_info
    • DO NOT filter by is_graduated - include both bonding curve (is_graduated=false) AND DEX (is_graduated=true) tokens
    • Add to newestTokens array (store full objects, not just addresses)

Example:

API_URL = MONAD_NETWORK === 'mainnet' ? 'https://api.nadapp.net' : 'https://dev-api.nad.fun'
response = GET ${API_URL}/order/creation_time?page=1&limit=50&is_nsfw=false&direction=DESC
decoded = base64.b64decode(response.text).decode('utf-8')
data = json.loads(decoded)

newestTokens = []  # Array of full token objects
For each token in data.tokens:
  # Include ALL tokens - both bonding curve AND DEX
  # Store FULL token object with token_info + market_info
  newestTokens.append({
    'token_info': token.token_info,
    'market_info': token.market_info,
    'percent': token.percent,
    'address': token.token_info.token_id  # For easy access
  })
Print: "✅ Method 7: Found X tokens with market data (bonding curve + DEX)"

Expected result: Array of full token objects (with token_info + market_info) - newest tokens including both bonding curve AND DEX

Response structure:

  • Same as Method 6: tokens array with token_info and market_info
  • direction=DESC returns newest first
  • CRITICAL: Include ALL tokens (both bonding curve AND DEX) - do NOT filter by is_graduated

Combine All Methods - CRITICAL STEP

What to do:

  1. Collect ALL token addresses from Methods 5, 6, and 7 into one set (removes duplicates)
  2. Count how many times each token appears across methods
  3. Sort tokens by frequency (tokens found in more methods = higher priority/confidence)
  4. Log summary of combination results

How to execute:

  1. Create collections:

    • allTokens = new Set() or allTokens = [] (use Set to avoid duplicates)
    • tokenFrequency = new Map() or {} to count occurrences
  2. Add tokens from each method:

    • Method 5: Add all token addresses from tokensFromEvents (New Events API) - these are just addresses
    • Method 6: Add all tokens from topMarketCapTokens (Market Cap API) - these are FULL objects with token_info + market_info
    • Method 7: Add all tokens from newestTokens (Creation Time API) - these are FULL objects with token_info + market_info

    IMPORTANT: For Methods 6 & 7, store the FULL token objects (not just addresses) so you can use market_info directly for analysis!

  3. Count frequency and preserve full data:

    • Use a Map/object: allTokensMap = {} where key = token address, value = token object with metadata
    • For Method 5 (addresses only): allTokensMap[address] = {address, source: 'method5', data: null}
    • For Method 6 (full objects): allTokensMap[address] = {address, source: 'method6', data: fullTokenObject}
    • For Method 7 (full objects): allTokensMap[address] = {address, source: 'method7', data: fullTokenObject}
    • Count frequency: tokenFrequency[address] = (tokenFrequency[address] || 0) + 1
    • CRITICAL: Preserve full token objects from Methods 6 & 7 - they contain market_info!
  4. Convert and sort:

    • candidateTokens = Object.values(allTokensMap) or Array.from(allTokensMap.values())
    • Sort by frequency DESC, then by data availability:
      candidateTokens.sort((a, b) => {
        const freqA = tokenFrequency[a.address] || 0
        const freqB = tokenFrequency[b.address] || 0
        if (freqB !== freqA) return freqB - freqA  // Higher frequency first
        // Prefer tokens with full data (market_info)
        const hasDataA = a.data && a.data.market_info ? 1 : 0
        const hasDataB = b.data && b.data.market_info ? 1 : 0
        return hasDataB - hasDataA
      })
      
    • prioritizedTokens = candidateTokens
  5. Log summary:

    • Print: "📊 Combined results: Total unique tokens: N"
    • Print: "Tokens found in 2+ methods: X"
    • Print: "Tokens found in all 3 methods: Y"
    • Print: "Tokens with full market data: Z"
    • Print: "Top 10 prioritized tokens: "

Example:

allTokensMap = {}  # Map: address -> {address, source, data}
tokenFrequency = {}

# Method 5: addresses only
for address in tokensFromEvents:
  allTokensMap[address] = {address: address, source: 'method5', data: null}
  tokenFrequency[address] = (tokenFrequency[address] || 0) + 1

# Method 6: full objects with market_info
for token in topMarketCapTokens:
  address = token.token_info.token_id
  allTokensMap[address] = {address: address, source: 'method6', data: token}
  tokenFrequency[address] = (tokenFrequency[address] || 0) + 1

# Method 7: full objects with market_info
for token in newestTokens:
  address = token.token_info.token_id
  allTokensMap[address] = {address: address, source: 'method7', data: token}
  tokenFrequency[address] = (tokenFrequency[address] || 0) + 1

# Sort by frequency and data availability
candidateTokens = Object.values(allTokensMap)
prioritizedTokens = candidateTokens.sort((a, b) => {
  freqDiff = tokenFrequency[b.address] - tokenFrequency[a.address]
  if (freqDiff !== 0) return freqDiff
  return (b.data && b.data.market_info ? 1 : 0) - (a.data && a.data.market_info ? 1 : 0)
})

Print: "📊 Combined: N unique tokens, X found in 2+ methods, Y found in all 3 methods, Z with full data"

Expected result: Prioritized array of token objects, each containing:

  • address: token address
  • source: which method(s) found it
  • data: full token object with token_info + market_info (if available from Methods 6 or 7)
  • foundInMethods: count from tokenFrequency

Save Found Tokens - CRITICAL STEP

What to do: After combining all methods and getting prioritizedTokens, you MUST save them to file $HOME/nadfunagent/found_tokens.json for manual review.

How to execute:

  1. Prepare token list:

    • Take top 50 tokens from prioritizedTokens
    • For each token, create object: {address: token, foundInMethods: frequency_count}
    • Create scan entry with timestamp, totalFound count, and tokens array
  2. Read existing file:

    • File: $HOME/nadfunagent/found_tokens.json
    • If file doesn't exist or is empty: start with []
    • If exists: parse JSON, get array
  3. Append and save:

    • Add new scan entry to array
    • Keep only last 100 scans (remove oldest if > 100)
    • Write back as formatted JSON

Example using Node.js script (RECOMMENDED):

# From skill root or repo root: print token addresses (one per line) and pipe to script
# prioritizedTokens is the array of token addresses; take first 50 and pass to script
echo -e "0x...\n0x..." | node scripts/save_tokens.js

Or from the agent: write the list of addresses to a temp file or stdin and run node scripts/save_tokens.js (script reads from stdin). Data dir: NADFUNAGENT_DATA_DIR or $HOME/nadfunagent.

Expected result: File $HOME/nadfunagent/found_tokens.json updated with latest scan results

2. Token Analysis - CRITICAL: Use Data from Methods 6 & 7 Directly

IMPORTANT: Methods 6 (Market Cap API) and Method 7 (Creation Time API) already return COMPLETE token data with market_info. Use this data directly for analysis - DO NOT make additional API calls unless absolutely necessary!

Data Structure from Methods 6 & 7: Each token in the response has this structure:

{
  "token_info": {
    "token_id": "0x...",
    "name": "...",
    "symbol": "...",
    "is_graduated": true/false,
    "created_at": timestamp,
    ...
  },
  "market_info": {
    "market_type": "DEX" or "BONDING_CURVE",
    "reserve_native": "1625513352353765005672133",  // Liquidity in MON (wei format)
    "reserve_token": "51582894169959918089536846",
    "token_price": "0.000886889894056452",
    "price": "0.0415501242",
    "price_usd": "0.000886889894056452",
    "volume": "761848094037806233587694495",  // Total volume (wei format)
    "holder_count": 31580,  // Number of holders
    "ath_price": "0.0128807779",
    "ath_price_usd": "0.0128807779",
    ...
  },
  "percent": 8.797  // Market cap change percentage
}

Analysis Steps:

  1. Extract data from Methods 6 & 7:

    • For each token in data.tokens array from Methods 6 & 7:
      • Extract token_info.token_id (token address)
      • Extract market_info.reserve_native (liquidity in wei, convert to MON: divide by 1e18)
      • Extract market_info.holder_count (number of holders)
      • Extract market_info.volume (total volume in wei, convert to MON: divide by 1e18)
      • Extract market_info.market_type ("DEX" or "BONDING_CURVE")
      • Extract token_info.is_graduated (true if graduated to DEX)
      • Extract percent (market cap change percentage)
  2. For Method 5 tokens (new-event API):

    • This API returns only basic event data without full market_info
    • For tokens from Method 5, you can optionally query /agent/market/:token_id to get market_info
    • BUT prioritize tokens from Methods 6 & 7 first (they already have complete data)
  3. Calculate Liquidity Score (30% weight):

    liquidityMON = parseFloat(market_info.reserve_native) / 1e18
    
    // Score based on liquidity tiers
    if (liquidityMON >= 1000) liquidityScore = 100      // Excellent
    else if (liquidityMON >= 500) liquidityScore = 80     // Very good
    else if (liquidityMON >= 100) liquidityScore = 60    // Good
    else if (liquidityMON >= 50) liquidityScore = 40      // Fair
    else if (liquidityMON >= 10) liquidityScore = 20      // Low
    else liquidityScore = 0                              // Too low
    
    weightedLiquidity = liquidityScore * 0.30
    
  4. Calculate Momentum Score (25% weight):

    // Use percent (market cap change) as momentum indicator
    percentChange = parseFloat(token.percent) || 0
    
    // Score based on positive momentum
    if (percentChange >= 50) momentumScore = 100      // Strong growth
    else if (percentChange >= 20) momentumScore = 80
    else if (percentChange >= 10) momentumScore = 60
    else if (percentChange >= 5) momentumScore = 40
    else if (percentChange >= 0) momentumScore = 20
    else momentumScore = 0                           // Negative momentum
    
    weightedMomentum = momentumScore * 0.25
    
  5. Calculate Volume Score (20% weight):

    volumeMON = parseFloat(market_info.volume) / 1e18
    
    // Score based on volume tiers
    if (volumeMON >= 1000000) volumeScore = 100      // 1M+ MON volume
    else if (volumeMON >= 500000) volumeScore = 80
    else if (volumeMON >= 100000) volumeScore = 60
    else if (volumeMON >= 50000) volumeScore = 40
    else if (volumeMON >= 10000) volumeScore = 20
    else volumeScore = 0
    
    weightedVolume = volumeScore * 0.20
    
  6. Calculate Holder Score (15% weight):

    holderCount = parseInt(market_info.holder_count) || 0
    
    // Score based on holder count
    if (holderCount >= 10000) holderScore = 100      // Excellent distribution
    else if (holderCount >= 5000) holderScore = 80
    else if (holderCount >= 1000) holderScore = 60
    else if (holderCount >= 500) holderScore = 40
    else if (holderCount >= 100) holderScore = 20
    else holderScore = 0
    
    weightedHolders = holderScore * 0.15
    
  7. Calculate Progress Score (10% weight):

    isGraduated = token_info.is_graduated === true
    marketType = market_info.market_type
    
    // Score based on market stage
    if (isGraduated || marketType === "DEX") {
      progressScore = 100  // Fully graduated to DEX
    } else {
      // Still on bonding curve - use percent as progress indicator
      // Higher percent = closer to graduation
      percent = parseFloat(token.percent) || 0
      if (percent >= 80) progressScore = 80
      else if (percent >= 50) progressScore = 60
      else if (percent >= 30) progressScore = 40
      else progressScore = 20
    }
    
    weightedProgress = progressScore * 0.10
    
  8. Calculate Authority Score (Bonus - up to +10 points):

    // Check for social media presence and website (indicates legitimacy)
    hasTwitter = token_info.twitter && token_info.twitter.length > 0
    hasTelegram = token_info.telegram && token_info.telegram.length > 0
    hasWebsite = token_info.website && token_info.website.length > 0
    
    authorityScore = 0
    if (hasTwitter) authorityScore += 3
    if (hasTelegram) authorityScore += 3
    if (hasWebsite) authorityScore += 4
    
    // Maximum +10 bonus points for full social presence
    authorityBonus = Math.min(authorityScore, 10)
    
  9. Calculate Total Score:

    totalScore = weightedLiquidity + weightedMomentum + weightedVolume + weightedHolders + weightedProgress + authorityBonus
    
    // Round to 2 decimal places
    totalScore = Math.round(totalScore * 100) / 100
    
  10. Store Analysis Results: For each token, store:

    {
      address: token_info.token_id,
      name: token_info.name,
      symbol: token_info.symbol,
      liquidity: liquidityMON,
      holders: holderCount,
      volume: volumeMON,
      marketType: marketType,
      isGraduated: isGraduated,
      percentChange: percentChange,
      scores: {
        liquidity: liquidityScore,
        momentum: momentumScore,
        volume: volumeScore,
        holders: holderScore,
        progress: progressScore,
        total: totalScore
      },
      foundInMethods: tokenFrequency[token_info.token_id] || 1
    }
    

CRITICAL INSTRUCTIONS:

  • Use data from Methods 6 & 7 DIRECTLY - they already contain all needed information
  • DO NOT make additional API calls to /agent/market/:token_id for tokens from Methods 6 & 7
  • Only for Method 5 tokens (if needed) make additional API calls
  • Calculate scores immediately after combining methods, before filtering
  • Check authority (social media presence): Check token_info.twitter, token_info.telegram, token_info.website - add bonus points
  • Log analysis results: Print "📊 Analysis: Token X has score Y (liquidity: A, momentum: B, volume: C, holders: D, progress: E, authority: +F)"

3. Filtering Criteria

Filter tokens based on:

  • Minimum liquidity: 5 MON
  • Minimum holders: 5
  • Bonding curve progress: >= 10%
  • Score calculation:
    • Liquidity: 30% weight
    • Momentum: 25% weight
    • Volume trend: 20% weight
    • Holder distribution: 15% weight
    • Bonding curve progress: 10% weight

4. Position Management - CRITICAL: Check Existing Positions FIRST

BEFORE analyzing new tokens, you MUST check and manage existing positions!

Step 1: Get Current Holdings

What to do:

  1. Get trading wallet address from MONAD_PRIVATE_KEY in .env (derive address from private key)
  2. Query current token holdings using Agent API: /agent/holdings/${walletAddress}?limit=100
  3. Filter tokens where balance > 0 (exclude zero balances)
  4. For each token with balance > 0, get current market data

How to execute:

// 1. Get wallet address from private key
const walletAddress = getAddressFromPrivateKey(MONAD_PRIVATE_KEY)

// 2. Query holdings
const holdingsResponse = await fetch(`${API_URL}/agent/holdings/${walletAddress}?limit=100`)
const holdings = await holdingsResponse.json()

// 3. Filter tokens with balance > 0
const activePositions = holdings.tokens.filter(token => {
  const balance = parseFloat(token.balance_info.balance) || 0
  return balance > 0
})

Print: "📊 Current positions: Found N tokens with balance > 0"

Step 2: Calculate P&L for Each Position

CRITICAL: Always use the check-pnl.js script from nadfun-trading skill for proper P&L calculation. This script:

  • Reads entry price (entryValueMON) from $HOME/nadfunagent/positions_report.json (automatically recorded by buy-token.js when you purchase)
  • Gets current value on-chain via nad.fun quote contract (or falls back to Agent API)
  • Calculates P&L: (currentValueMON - entryValueMON) / entryValueMON * 100

What to do:

  1. Use the script: Run check-pnl.js from nadfun-trading skill directory
  2. The script reads entry prices from JSON (set by buy-token.js after purchases)
  3. Gets current value on-chain via nad.fun quote contract
  4. Calculates real P&L based on actual entry price

How to execute:

# Check P&L for all positions
cd $HOME/.openclaw/workspace/skills/nadfun-trading
node check-pnl.js

# Or with auto-sell (sells if P&L >= +5% or <= -10%)
node check-pnl.js --auto-sell

Manual calculation (if script unavailable):

// Load entry prices from positions_report.json
const report = JSON.parse(await fs.readFile('$HOME/nadfunagent/positions_report.json', 'utf-8'))

for (const position of activePositions) {
  const tokenAddress = position.token_info.token_id || position.token_info.address
  const tokenBalance = parseFloat(position.balance_info.balance) || 0
  
  // Get entry price from JSON (recorded by buy-token.js)
  const prev = report.positions?.find(p => 
    (p.address || '').toLowerCase() === tokenAddress.toLowerCase()
  )
  const entryValueMON = prev?.entryValueMON || 0
  
  // Get current value on-chain via nad.fun quote contract
  const [router, amountOutWei] = await publicClient.readContract({
    address: '0x7e78A8DE94f21804F7a17F4E8BF9EC2c872187ea', // nad.fun quote contract
    abi: lensAbi,
    functionName: 'getAmountOut',
    args: [tokenAddress, parseEther(tokenBalance.toString()), false] // false = selling
  })
  const currentValueMON = Number(amountOutWei) / 1e18
  
  // Calculate P&L
  const pnlPercent = entryValueMON > 0
    ? ((currentValueMON - entryValueMON) / entryValueMON) * 100
    : 0
  
  positions.push({
    address: tokenAddress,
    balance: tokenBalance,
    entryValueMON: entryValueMON,
    currentValueMON: currentValueMON,
    pnlPercent: pnlPercent
  })
}

Print: "💰 Position P&L calculated for N positions (source: on-chain nad.fun quote + positions_report.json)"

Entry Price Tracking:

  • Entry price is automatically recorded by buy-token.js after successful purchase
  • Stored in $HOME/nadfunagent/positions_report.json as entryValueMON
  • If entry price not found, check-pnl.js uses current value as fallback (P&L = 0%)

Step 3: Make Sell Decisions

What to do: For each position, check sell conditions:

  1. Stop-Loss Check:

    if (pnlPercent <= -10) {  // STOP_LOSS_PERCENT = -10%
      // SELL ALL - stop loss triggered
      sellDecision = {
        action: 'SELL_ALL',
        reason: 'Stop-loss triggered',
        tokenAddress: position.address,
        amount: 'all'
      }
    }
    
  2. Take-Profit Check:

    if (pnlPercent >= 20) {  // PROFIT_TARGET_PERCENT = 20%
      // SELL HALF - take profit
      sellDecision = {
        action: 'SELL_HALF',
        reason: 'Take-profit triggered',
        tokenAddress: position.address,
        amount: position.balance / 2
      }
    }
    
  3. Trailing Stop (Optional):

    // If profit was > 15% but dropped to < 5%, sell to protect gains
    if (previousPnL > 15 && pnlPercent < 5) {
      sellDecision = {
        action: 'SELL_ALL',
        reason: 'Trailing stop - protecting gains',
        tokenAddress: position.address,
        amount: 'all'
      }
    }
    

How to execute:

const sellDecisions = []

for (const position of positions) {
  // Stop-loss: sell all if down 10%+
  if (position.pnlPercent <= -10) {
    sellDecisions.push({
      tokenAddress: position.address,
      action: 'SELL_ALL',
      reason: `Stop-loss: ${position.pnlPercent.toFixed(2)}%`,
      amount: 'all'
    })
  }
  // Take-profit: sell half if up 20%+
  else if (position.pnlPercent >= 20) {
    sellDecisions.push({
      tokenAddress: position.address,
      action: 'SELL_HALF',
      reason: `Take-profit: ${position.pnlPercent.toFixed(2)}%`,
      amount: position.balance / 2
    })
  }
}

Print: "🔔 Sell decisions: N positions need action"
for (const decision of sellDecisions) {
  Print: `   ${decision.action}: ${decision.tokenAddress} - ${decision.reason}`
}

Step 4: Execute Sell Orders

What to do: For each sell decision:

  1. Use nadfun-trading skill with action sell
  2. Parameters: token address, amount (from decision)
  3. The skill handles: balance check, quote, approve, execution

How to execute:

for (const decision of sellDecisions) {
  try {
    // Use nadfun-trading skill
    await useSkill('nadfun-trading', {
      action: 'sell',
      token: decision.tokenAddress,
      amount: decision.amount  // 'all' or specific amount
    })
    
    Print: `✅ Sold ${decision.amount} of ${decision.tokenAddress}: ${decision.reason}`
  } catch (error) {
    Print: `❌ Failed to sell ${decision.tokenAddress}: ${error.message}`
  }
}

5. Trading Execution - Buy New Tokens

CRITICAL: Only buy new tokens AFTER managing existing positions!

Buy Decision Logic:

  1. Prioritize tokens with authority (social media presence):

    • Tokens with Twitter + Telegram + Website get priority
    • These are more legitimate and have better community support
  2. Consider both early-stage AND established tokens:

    • Early-stage (5-50 MON liquidity): Higher risk, higher potential
    • Established (50k+ MON liquidity): Lower risk, steady growth
    • Both can be profitable if they meet score criteria
  3. Buy Criteria:

    • Score >= 60 (or >= 55 if has social media)
    • Liquidity >= 5 MON
    • Holders >= 5
    • NOT already in portfolio (check existing positions first)
  4. Position Sizing:

    • For tokens with authority (social media): up to 0.15 MON
    • For tokens without authority: up to 0.1 MON
    • Maximum total position per token: 0.15 MON

CRITICAL: Always use the buy-token.js script from nadfun-trading skill. This script:

  • Automatically detects bonding curve vs DEX via nad.fun quote contract
  • Handles DEX: wraps MON→WMON, approves, swaps
  • Automatically records entry price in $HOME/nadfunagent/positions_report.json after successful purchase
  • Works on bonding curve (MON) or DEX (MON) - all trading uses MON

Buy tokens:

cd $HOME/.openclaw/workspace/skills/nadfun-trading
NAD_PRIVATE_KEY=$MONAD_PRIVATE_KEY node buy-token.js <token-address> <MON-amount> [--slippage=300]

Example:

NAD_PRIVATE_KEY=0x... node buy-token.js 0x123...abc 0.15 --slippage=300

After purchase:

  • Entry price (entryValueMON) is automatically recorded in $HOME/nadfunagent/positions_report.json
  • This entry price is used by check-pnl.js for P&L calculation
  • No manual tracking needed - everything is automated

Risk management:

  • CRITICAL: Buy tokens on BOTH bonding curve AND DEX (don't filter by market_type)
  • The nadfun-trading skill automatically detects market type and uses correct contract
  • For all trades, use MON balance (no wrapping needed - all trading uses MON)
  • Set slippage tolerance: 2-3% (increased for better execution, especially on DEX)
  • Set deadline: 5 minutes from now
  • Don't exceed MAX_POSITION_SIZE per token

6. Profit Distribution

CRITICAL: Use MMIND_TOKEN_ADDRESS from $HOME/nadfunagent/.env file.

When profit >= 0.1 MON:

// Step 1: Load MMIND_TOKEN_ADDRESS from .env file
const envFile = await readFile('$HOME/nadfunagent/.env', 'utf-8')
const mmindMatch = envFile.match(/MMIND_TOKEN_ADDRESS=(0x[a-fA-F0-9]+)/)
if (!mmindMatch) {
  throw new Error('MMIND_TOKEN_ADDRESS not found in .env file')
}
const MMIND_TOKEN_ADDRESS = mmindMatch[1]

// Step 2: Get MMIND token holders via Indexer
// Query Transfer events to find all addresses that ever held MMIND
const latestBlock = await publicClient.getBlockNumber()
const safeLatest = latestBlock - 10n

// Query in chunks (RPC limit: ~100 blocks)
const transfers = []
for (let from = 0n; from < safeLatest; from += 10000n) {
  const to = from + 10000n > safeLatest ? safeLatest : from + 10000n
  try {
    const events = await useSkill('nadfun-indexer', {
      event: 'Transfer',
      token: MMIND_TOKEN_ADDRESS,
      fromBlock: from,
      toBlock: to
    })
    transfers.push(...events)
  } catch (err) {
    // Continue if chunk fails
  }
}

// Step 3: Collect unique addresses
const holderAddresses = new Set()
transfers.forEach(event => {
  if ('args' in event) {
    if (event.args.from && event.args.from !== '0x0000000000000000000000000000000000000000') {
      holderAddresses.add(event.args.from)
    }
    if (event.args.to && event.args.to !== '0x0000000000000000000000000000000000000000') {
      holderAddresses.add(event.args.to)
    }
  }
})

// Step 4: Get current balances for each holder
const distributions = []
for (const address of holderAddresses) {
  try {
    const balance = await publicClient.readContract({
      address: MMIND_TOKEN_ADDRESS,
      abi: erc20Abi,
      functionName: 'balanceOf',
      args: [address]
    })
    
    if (balance > 0n) {
      distributions.push({ address, balance })
    }
  } catch (err) {
    // Skip if balance check fails
  }
}

// Step 5: Get total supply
const totalSupply = await publicClient.readContract({
  address: MMIND_TOKEN_ADDRESS,
  abi: erc20Abi,
  functionName: 'totalSupply'
})

// Step 6: Calculate and distribute profit proportionally
const profitToDistribute = (profit * BigInt(PNL_DISTRIBUTION_PERCENT)) / 100n

for (const holder of distributions) {
  const share = (profitToDistribute * holder.balance) / totalSupply
  if (share >= parseEther('0.001')) { // Minimum 0.001 MON
    await transferMON(holder.address, share) // Transfer MON directly (all trading uses MON)
  }
}

Autonomous Trading Loop

The agent runs continuously:

  1. Position Management FIRST (CRITICAL - do this before scanning)

    • Get wallet address from MONAD_PRIVATE_KEY
    • Query current holdings: /agent/holdings/${walletAddress}?limit=100
    • Filter tokens with balance > 0
    • For each position: use check-pnl.js to get real P&L (reads entry price from $HOME/nadfunagent/positions_report.json, gets current value on-chain via nad.fun quote contract)
    • Execute sell orders: use sell-token.js or check-pnl.js --auto-sell for stop-loss (P&L <= -10%) or take-profit (P&L >= +5%)
    • Log: "📊 Positions: N checked, X sold; entry prices tracked in positions_report.json"
  2. Scan market (after managing positions, every 10 minutes to avoid rate limits)

    • Method 5: Fetch new events API (/api/token/new-event) for real-time BUY/CREATE events
    • Method 6: Fetch top tokens by market cap (api.nadapp.net/order/market_cap, base64 decoded)
    • Method 7: Fetch newest tokens (api.nadapp.net/order/creation_time, base64 decoded)
    • Combine all methods and prioritize tokens found in multiple sources
    • Add 2-3 second delays between API calls to respect rate limits (CRITICAL to avoid HTTP 429)
  3. Analyze opportunities

    • Use data DIRECTLY from Methods 6 & 7 (already contains complete market_info)
    • Calculate scores using market_info: liquidity (30%), momentum (25%), volume (20%), holders (15%), progress (10%)
    • Convert wei values to MON: reserve_native / 1e18, volume / 1e18
    • Filter by criteria (min liquidity 5 MON, min holders 5, min score 60)
    • Sort by score DESC, then by methods count, then by liquidity
    • Take top 20 candidates
    • Generate buy/sell signals
  4. Execute trades

    • Buy on bonding curve (MON) or DEX (MON) - all trading uses MON
    • Monitor positions
    • Stop-loss and take-profit
  5. Distribute profits

    • Calculate total profit
    • Get MMIND holders via Indexer (Transfer events)
    • Distribute proportionally (30% of profit) in MON

Risk Management

  • Position sizing: Based on confidence score (max 0.1 MON)
  • Stop-loss: -10% (automatic sell)
  • Take-profit: +20% (sell half position)
  • Slippage protection: 1-2% tolerance
  • Minimum liquidity: 5 MON required

Usage Examples

Start autonomous trading:

# Via OpenClaw chat
"Start autonomous trading agent"

# Or via cron job (runs every minute). Paths: use NADFUN_ENV_PATH / NADFUNAGENT_DATA_DIR for .env and data; run scripts from nadfun-trading skill directory (clawhub install).
openclaw cron add \
  --name "Nad.fun Trading Agent" \
  --cron "* * * * *" \
  --session isolated \
  --message "Run autonomous trading cycle: 1) Load config from .env (path: NADFUN_ENV_PATH or NADFUNAGENT_DATA_DIR/.env; need MMIND_TOKEN_ADDRESS, MONAD_PRIVATE_KEY, MONAD_RPC_URL, MONAD_NETWORK). 2) From nadfun-trading skill directory run: node execute-bonding-v2.js (uses check-pnl.js for P&L from positions_report.json at POSITIONS_REPORT_PATH or NADFUNAGENT_DATA_DIR, auto-sells at +5% or -10%). 3) If there is positive PnL (profit >= 0.1 MON), distribute profits to MMIND token holders: use MMIND_TOKEN_ADDRESS from .env, get holders via indexer/Transfer events, distribute proportionally (e.g. 30%) in MON. Report output in English."

Check agent status:

# Via OpenClaw chat
"Show trading agent status and statistics"

View found tokens: (data dir: NADFUNAGENT_DATA_DIR, default $HOME/nadfunagent)

# View latest found tokens
cat "${NADFUNAGENT_DATA_DIR:-$HOME/nadfunagent}/found_tokens.json" | jq '.[-1]'

# View all tokens from last scan
cat "${NADFUNAGENT_DATA_DIR:-$HOME/nadfunagent}/found_tokens.json" | jq '.[-1].tokens[] | .address'

# View tokens found in multiple methods (higher confidence)
cat "${NADFUNAGENT_DATA_DIR:-$HOME/nadfunagent}/found_tokens.json" | jq '.[-1].tokens[] | select(.foundInMethods > 1) | .address'

# View summary of last 10 scans
cat "${NADFUNAGENT_DATA_DIR:-$HOME/nadfunagent}/found_tokens.json" | jq '.[-10:] | .[] | {timestamp, totalFound}'

Manual trade:

# Via OpenClaw chat
"Buy 0.1 MON worth of token 0x..."
"Sell all tokens for 0x..."

Integration with OpenClaw

This skill integrates with:

  • Cron Jobs: Schedule autonomous trading cycles
  • Skills: Uses nadfun-trading, nadfun-indexer, nadfun-agent-api
  • Gateway: Runs via OpenClaw Gateway
  • Channels: Can send notifications via Telegram/WhatsApp/etc.

Error Handling

  • All errors are logged but don't stop the agent
  • Failed trades are retried with exponential backoff
  • API failures fall back to Indexer queries
  • Network errors trigger fallback RPC providers

Rate Limit Handling

CRITICAL: Handle rate limits gracefully to avoid HTTP 429 errors.

API Rate Limits

Nad.fun Agent API:

  • Without API Key: 10 requests/minute (IP-based)
  • With API Key: 100 requests/minute (Key-based)
  • Solution: Add delays between requests (min 2 seconds between calls)

Nad.fun Public APIs (nad.fun/api/* and api.nadapp.net/*):

  • Rate limits are not publicly documented but appear to be IP-based
  • Solution: Add 2-3 second delays between calls, especially for base64-decoded endpoints
  • Handle HTTP 429 errors gracefully with exponential backoff

Anthropic/Claude API (CRITICAL - This is the main source of HTTP 429):

  • Rate limits vary by tier (typically 50-200 requests per minute)
  • Problem: Each agent execution makes multiple requests to Claude
  • Solution:
    • Cron job runs every 10 minutes (*/10 * * * *) to reduce frequency
    • Optimize agent logic to minimize token usage
    • Batch operations when possible
    • If still hitting limits, increase to 15 minutes (*/15 * * * *)

Rate Limit Strategy

  1. Add delays between API calls (CRITICAL):

    // Wait 2-3 seconds between Nad.fun API calls
    await new Promise(resolve => setTimeout(resolve, 2000))
    
    // Wait 1 second between Indexer queries
    await new Promise(resolve => setTimeout(resolve, 1000))
    
  2. Limit parallel requests:

    • Process tokens sequentially or in small batches (max 5 at a time)
    • Add delays between batches
    • Don't make all 7 methods run simultaneously - stagger them
  3. Handle 429 errors gracefully:

    async function fetchWithRetry(url, options, maxRetries = 2) {
      for (let i = 0; i < maxRetries; i++) {
        try {
          const result = await fetch(url, options)
          if (result.status === 429) {
            const retryAfter = parseInt(result.headers.get('Retry-After') || '60')
            console.log(`Rate limited, waiting ${retryAfter} seconds...`)
            await new Promise(resolve => setTimeout(resolve, retryAfter * 1000))
            continue
          }
          return result
        } catch (err) {
          if (i === maxRetries - 1) throw err
          await new Promise(resolve => setTimeout(resolve, 5000 * (i + 1)))
        }
      }
    }
    
  4. Optimize agent execution:

    • Use Indexer methods (1-2) first (they're faster and don't hit API limits)
    • Only use API methods (3-7) if Indexer doesn't find enough tokens
    • Analyze ALL tokens that pass filters (no limit)
    • Skip detailed analysis if token doesn't meet basic filters
  5. Cron frequency:

    • Current: every 10 minutes (*/10 * * * *)
    • If still hitting Claude API limits, increase to 15 minutes (*/15 * * * *)

Reviews (0)

Sign in to write a review.

No reviews yet. Be the first to review!

Comments (0)

Sign in to join the discussion.

No comments yet. Be the first to share your thoughts!

Compatible Platforms

Pricing

Free

Related Configs