Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {}
}
3 changes: 3 additions & 0 deletions scripts/add_wallet_address.sql
Original file line number Diff line number Diff line change
@@ -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);
3 changes: 3 additions & 0 deletions scripts/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
10 changes: 9 additions & 1 deletion src/routes/agents.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/services/AgentService.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]
);
Expand Down
88 changes: 88 additions & 0 deletions src/services/MoltRankService.js
Original file line number Diff line number Diff line change
@@ -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<Object|null>} 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;