Install
openclaw skills install nadfunagentAutonomous Nad.fun trading agent that scans markets via API and indexer, analyzes tokens, executes trades, and shares profits with MMIND token holders.
openclaw skills install nadfunagentCRITICAL COMMUNICATION RULES:
.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.Autonomous trading agent that scans Nad.fun markets, analyzes tokens using momentum strategies, executes trades, and distributes profits to MMIND token holders.
monad-development skill installed (for wallet and RPC setup)nadfun-trading skill installed (for buy/sell operations), or use the trading/ folder from this reponadfun-indexer skill installed (for querying events)nadfun-agent-api skill installed (for market data)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.
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:
.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.MMIND_TOKEN_ADDRESS - use this for profit distributionMONAD_PRIVATE_KEY - use this for wallet operationsMONAD_RPC_URL - use this for blockchain queriesMONAD_NETWORK - determines API endpoints (mainnet: api.nadapp.net, testnet: dev-api.nad.fun)EXECUTION SUMMARY - READ THIS FIRST:
$HOME/nadfunagent/.env (MMIND_TOKEN_ADDRESS, MONAD_PRIVATE_KEY, MONAD_RPC_URL, MONAD_NETWORK)$HOME/nadfunagent/found_tokens.json for manual reviewCRITICAL INSTRUCTIONS: You MUST scan tokens using ALL 7 methods below. Execute each method step-by-step and collect ALL found token addresses.
EXECUTION ORDER:
STEP-BY-STEP EXECUTION:
CRITICAL LOGGING REQUIREMENT: After executing EACH method, you MUST log:
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:
$HOME/nadfunagent/.env to get MONAD_RPC_URL and MONAD_NETWORKeth_blockNumbersafeLatest = currentBlock - 10 (safety margin)nadfun-indexer skillHow to execute:
nadfun-indexer skill with parameters: event=CurveCreate, fromBlock=<start>, toBlock=<end>event.args.token or event.args[1] (token is second argument)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
STATUS CHECK: This method requires RPC access. You MUST execute it, don't skip it!
What to do:
How to execute:
nadfun-indexer skill: event=CurveBuy, fromBlock=<start>, toBlock=<end>publicClient.getContractEvents({ address: CURVE_ADDRESS, eventName: "CurveBuy", fromBlock, toBlock })token = event.args.token or event.args[1]amountIn = event.args.amountIn or event.args[2] (amount in MON)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
What to do:
How to execute:
MONAD_NETWORK=mainnet then https://api.nadapp.net, else https://dev-api.nad.fun${API_URL}/agent/swap-history/${tokenAddress}?limit=50&trade_type=ALLresponse.swaps:
swap.swap_info.timestamp - if within last 24 hoursswap.swap_info.native_amount (in wei, convert to MON: divide by 1e18)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
STATUS CHECK: Don't skip this method! Execute it with rate limit delays.
What to do:
How to execute:
amountIn > 1000000000000000000 (1 MON in wei = 1e18)sender address: event.args.sender or event.args[0]${API_URL}/agent/holdings/${address}?limit=50Accept: application/jsonholding in response.tokens:
holding.balance_info.balance - if > 0holding.token_info.address or holding.token_info.token_idExample:
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
STATUS: ✅ This method is WORKING! Found 7 tokens in last scan.
What to do:
/api/token/new-event endpointHow to execute:
MONAD_NETWORK=mainnet: baseUrl = 'https://nad.fun'baseUrl = 'https://dev.nad.fun'${baseUrl}/api/token/new-eventAccept: application/json, User-Agent: OpenClaw-Agent/1.0event.type === 'CREATE': extract event.token_info.token_idevent.type === 'BUY': extract event.token_info.token_idevent.type === 'SELL': optionally extract (indicates active trading)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 actionSTATUS CHECK: Previous error: base64 decoding failed. Fix: Use proper decoding method.
What to do:
How to execute:
${API_URL}/order/market_cap?page=1&limit=50&is_nsfw=false
API_URL = MONAD_NETWORK === 'mainnet' ? 'https://api.nadapp.net' : 'https://dev-api.nad.fun'Accept: application/json, User-Agent: OpenClaw-Agent/1.0import base64; decoded = base64.b64decode(response_text).decode('utf-8')echo "$response_text" | base64 -d{ "tokens": [...], "total_count": N }data.tokens:
token.token_info.token_id (token address)token_info AND market_info{token_info: {...}, market_info: {...}, percent: ...}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_infototal_count: Total number of tokenstoken_info (token_id, name, symbol, creator, etc.) and market_info (market_type, price, volume, holder_count, etc.)CRITICAL: Include BOTH bonding curve AND DEX tokens! Do NOT filter by is_graduated.
What to do:
How to execute:
${API_URL}/order/creation_time?page=1&limit=50&is_nsfw=false&direction=DESCdata.tokens arraytoken.token_info.token_id (token address)token_info AND market_infoExample:
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:
tokens array with token_info and market_infodirection=DESC returns newest firstWhat to do:
How to execute:
Create collections:
allTokens = new Set() or allTokens = [] (use Set to avoid duplicates)tokenFrequency = new Map() or {} to count occurrencesAdd tokens from each method:
IMPORTANT: For Methods 6 & 7, store the FULL token objects (not just addresses) so you can use market_info directly for analysis!
Count frequency and preserve full data:
allTokensMap = {} where key = token address, value = token object with metadataallTokensMap[address] = {address, source: 'method5', data: null}allTokensMap[address] = {address, source: 'method6', data: fullTokenObject}allTokensMap[address] = {address, source: 'method7', data: fullTokenObject}tokenFrequency[address] = (tokenFrequency[address] || 0) + 1Convert and sort:
candidateTokens = Object.values(allTokensMap) or Array.from(allTokensMap.values())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 = candidateTokensLog summary:
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 addresssource: which method(s) found itdata: full token object with token_info + market_info (if available from Methods 6 or 7)foundInMethods: count from tokenFrequencyWhat 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:
Prepare token list:
{address: token, foundInMethods: frequency_count}Read existing file:
$HOME/nadfunagent/found_tokens.json[]Append and save:
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
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:
Extract data from Methods 6 & 7:
data.tokens array from Methods 6 & 7:
token_info.token_id (token address)market_info.reserve_native (liquidity in wei, convert to MON: divide by 1e18)market_info.holder_count (number of holders)market_info.volume (total volume in wei, convert to MON: divide by 1e18)market_info.market_type ("DEX" or "BONDING_CURVE")token_info.is_graduated (true if graduated to DEX)percent (market cap change percentage)For Method 5 tokens (new-event API):
/agent/market/:token_id to get market_infoCalculate 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
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
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
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
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
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)
Calculate Total Score:
totalScore = weightedLiquidity + weightedMomentum + weightedVolume + weightedHolders + weightedProgress + authorityBonus
// Round to 2 decimal places
totalScore = Math.round(totalScore * 100) / 100
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:
/agent/market/:token_id for tokens from Methods 6 & 7token_info.twitter, token_info.telegram, token_info.website - add bonus pointsFilter tokens based on:
BEFORE analyzing new tokens, you MUST check and manage existing positions!
What to do:
MONAD_PRIVATE_KEY in .env (derive address from private key)/agent/holdings/${walletAddress}?limit=100balance > 0 (exclude zero balances)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"
CRITICAL: Always use the check-pnl.js script from nadfun-trading skill for proper P&L calculation. This script:
entryValueMON) from $HOME/nadfunagent/positions_report.json (automatically recorded by buy-token.js when you purchase)(currentValueMON - entryValueMON) / entryValueMON * 100What to do:
check-pnl.js from nadfun-trading skill directorybuy-token.js after purchases)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:
buy-token.js after successful purchase$HOME/nadfunagent/positions_report.json as entryValueMONcheck-pnl.js uses current value as fallback (P&L = 0%)What to do: For each position, check sell conditions:
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'
}
}
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
}
}
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}`
}
What to do: For each sell decision:
nadfun-trading skill with action sellHow 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}`
}
}
CRITICAL: Only buy new tokens AFTER managing existing positions!
Buy Decision Logic:
Prioritize tokens with authority (social media presence):
Consider both early-stage AND established tokens:
Buy Criteria:
Position Sizing:
CRITICAL: Always use the buy-token.js script from nadfun-trading skill. This script:
$HOME/nadfunagent/positions_report.json after successful purchaseBuy 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:
entryValueMON) is automatically recorded in $HOME/nadfunagent/positions_report.jsoncheck-pnl.js for P&L calculationRisk management:
nadfun-trading skill automatically detects market type and uses correct contractCRITICAL: 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)
}
}
The agent runs continuously:
Position Management FIRST (CRITICAL - do this before scanning)
MONAD_PRIVATE_KEY/agent/holdings/${walletAddress}?limit=100check-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)sell-token.js or check-pnl.js --auto-sell for stop-loss (P&L <= -10%) or take-profit (P&L >= +5%)Scan market (after managing positions, every 10 minutes to avoid rate limits)
/api/token/new-event) for real-time BUY/CREATE eventsapi.nadapp.net/order/market_cap, base64 decoded)api.nadapp.net/order/creation_time, base64 decoded)Analyze opportunities
Execute trades
Distribute profits
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..."
This skill integrates with:
CRITICAL: Handle rate limits gracefully to avoid HTTP 429 errors.
Nad.fun Agent API:
Nad.fun Public APIs (nad.fun/api/* and api.nadapp.net/*):
Anthropic/Claude API (CRITICAL - This is the main source of HTTP 429):
*/10 * * * *) to reduce frequency*/15 * * * *)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))
Limit parallel requests:
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)))
}
}
}
Optimize agent execution:
Cron frequency:
*/10 * * * *)*/15 * * * *)