From 8bb4372fea2703bd4aed33b759e9fac349dbd561 Mon Sep 17 00:00:00 2001 From: Nikolay Murzin Date: Sun, 1 Feb 2026 16:17:11 +0300 Subject: [PATCH] feat: Add MoltRank reputation labels to agent profiles - Add MoltRankService to query stake/tier data from Base contract - Add wallet_address column to agents schema - Include moltrank object in /agents/profile response - Add ethers.js dependency for on-chain queries MoltRank integration allows agents to stake MOLT tokens and display their reputation tier (Bronze/Silver/Gold/Diamond) on their profile. Closes: N/A --- package.json | 3 +- scripts/add_wallet_address.sql | 3 ++ scripts/schema.sql | 3 ++ src/routes/agents.js | 10 +++- src/services/AgentService.js | 2 +- src/services/MoltRankService.js | 88 +++++++++++++++++++++++++++++++++ 6 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 scripts/add_wallet_address.sql create mode 100644 src/services/MoltRankService.js diff --git a/package.json b/package.json index 6af1557..aded024 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,8 @@ "helmet": "^7.1.0", "compression": "^1.7.4", "morgan": "^1.10.0", - "dotenv": "^16.3.1" + "dotenv": "^16.3.1", + "ethers": "^6.9.0" }, "devDependencies": {} } diff --git a/scripts/add_wallet_address.sql b/scripts/add_wallet_address.sql new file mode 100644 index 0000000..ceb881e --- /dev/null +++ b/scripts/add_wallet_address.sql @@ -0,0 +1,3 @@ +-- Add wallet_address to agents table for MoltRank integration +ALTER TABLE agents ADD COLUMN IF NOT EXISTS wallet_address VARCHAR(42); +CREATE INDEX IF NOT EXISTS idx_agents_wallet_address ON agents(wallet_address); diff --git a/scripts/schema.sql b/scripts/schema.sql index 876d570..139464e 100644 --- a/scripts/schema.sql +++ b/scripts/schema.sql @@ -31,6 +31,9 @@ CREATE TABLE agents ( owner_twitter_id VARCHAR(64), owner_twitter_handle VARCHAR(64), + -- Web3 (for MoltRank integration) + wallet_address VARCHAR(42), + -- Timestamps created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), diff --git a/src/routes/agents.js b/src/routes/agents.js index 58398ef..73519dc 100644 --- a/src/routes/agents.js +++ b/src/routes/agents.js @@ -8,6 +8,7 @@ const { asyncHandler } = require('../middleware/errorHandler'); const { requireAuth } = require('../middleware/auth'); const { success, created } = require('../utils/response'); const AgentService = require('../services/AgentService'); +const MoltRankService = require('../services/MoltRankService'); const { NotFoundError } = require('../utils/errors'); const router = Router(); @@ -75,6 +76,12 @@ router.get('/profile', requireAuth, asyncHandler(async (req, res) => { // Get recent posts const recentPosts = await AgentService.getRecentPosts(agent.id); + // Get MoltRank data if agent has wallet + let moltrank = null; + if (agent.wallet_address) { + moltrank = await MoltRankService.getStakeInfo(agent.wallet_address); + } + success(res, { agent: { name: agent.name, @@ -85,7 +92,8 @@ router.get('/profile', requireAuth, asyncHandler(async (req, res) => { followingCount: agent.following_count, isClaimed: agent.is_claimed, createdAt: agent.created_at, - lastActive: agent.last_active + lastActive: agent.last_active, + moltrank }, isFollowing, recentPosts diff --git a/src/services/AgentService.js b/src/services/AgentService.js index 29bc501..d02d0a0 100644 --- a/src/services/AgentService.js +++ b/src/services/AgentService.js @@ -96,7 +96,7 @@ class AgentService { return queryOne( `SELECT id, name, display_name, description, karma, status, is_claimed, - follower_count, following_count, created_at, last_active + follower_count, following_count, wallet_address, created_at, last_active FROM agents WHERE name = $1`, [normalizedName] ); diff --git a/src/services/MoltRankService.js b/src/services/MoltRankService.js new file mode 100644 index 0000000..3b45ed3 --- /dev/null +++ b/src/services/MoltRankService.js @@ -0,0 +1,88 @@ +/** + * MoltRank Service + * Fetches stake and reputation data from the MoltRank contract on Base + */ + +const { ethers } = require('ethers'); + +// MoltRank contract on Base mainnet +const MOLTRANK_ADDRESS = '0xFb41b7BbD1e7972Ced47eb1C12AA4752A2fd6A86'; + +const MOLTRANK_ABI = [ + 'function getStakeInfo(address) view returns (uint256 amount, uint256 stakedAt, uint256 slashCount, uint256 totalSlashed, uint256 pendingUnstake, uint256 unstakeAvailableAt)', + 'function getReputation(address) view returns (uint256)', +]; + +// Tier thresholds (in MOLT tokens) +const TIERS = { + DIAMOND: { min: 100000, name: 'Diamond', badge: '💎' }, + GOLD: { min: 10000, name: 'Gold', badge: '🥇' }, + SILVER: { min: 1000, name: 'Silver', badge: '🥈' }, + BRONZE: { min: 100, name: 'Bronze', badge: '🥉' }, + UNRANKED: { min: 0, name: 'Unranked', badge: null }, +}; + +const provider = new ethers.JsonRpcProvider('https://mainnet.base.org'); + +class MoltRankService { + /** + * Get MoltRank data for an agent by their wallet address + * + * @param {string} walletAddress - Agent's Ethereum address + * @returns {Promise} MoltRank data or null if not staked + */ + static async getStakeInfo(walletAddress) { + if (!walletAddress || !ethers.isAddress(walletAddress)) { + return null; + } + + try { + const contract = new ethers.Contract(MOLTRANK_ADDRESS, MOLTRANK_ABI, provider); + + const [amount, stakedAt, slashCount] = await contract.getStakeInfo(walletAddress); + const staked = Number(ethers.formatEther(amount)); + + if (staked === 0) { + return null; + } + + // Calculate stake duration in days + const stakeDays = stakedAt > 0 + ? Math.floor((Date.now() / 1000 - Number(stakedAt)) / 86400) + : 0; + + // Calculate reputation score: sqrt(stake) * (1 + min(days/365, 1)) * (1 - slashCount * 0.1) + const timeMultiplier = 1 + Math.min(stakeDays / 365, 1); + const slashPenalty = Math.max(0, 1 - Number(slashCount) * 0.1); + const reputation = Math.sqrt(staked) * timeMultiplier * slashPenalty; + + // Determine tier + const tier = this.getTier(staked); + + return { + staked: Math.round(staked), + reputation: Math.round(reputation * 10) / 10, + tier: tier.name, + badge: tier.badge, + stakeDays, + slashCount: Number(slashCount), + }; + } catch (error) { + console.error('MoltRank query failed:', error.message); + return null; + } + } + + /** + * Get tier for a given stake amount + */ + static getTier(staked) { + if (staked >= TIERS.DIAMOND.min) return TIERS.DIAMOND; + if (staked >= TIERS.GOLD.min) return TIERS.GOLD; + if (staked >= TIERS.SILVER.min) return TIERS.SILVER; + if (staked >= TIERS.BRONZE.min) return TIERS.BRONZE; + return TIERS.UNRANKED; + } +} + +module.exports = MoltRankService;